Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a "Structuring Reducers" recipe #1784

Closed
12 tasks done
markerikson opened this issue May 30, 2016 · 44 comments
Closed
12 tasks done

Add a "Structuring Reducers" recipe #1784

markerikson opened this issue May 30, 2016 · 44 comments
Assignees
Labels

Comments

@markerikson
Copy link
Contributor

markerikson commented May 30, 2016

We really, really need a page talking about approaches, guidelines, and suggestions for organizing reducer logic. I've been saying I want to write this for a while, but have been too busy so far. I'm still up for it, but further suggestions and/or offers of help would be appreciated.

WIP page: https://github.com/markerikson/redux/blob/structuring-reducers-page/docs/recipes/StructuringReducers.md

Initial sketch of possible topics:

  • Understanding that you really only have one reducer function, which is just subdivided for maintainability and simplicity
  • Understanding "reducer composition" and that you can break things down using plain ol' functions
  • The basic rules of reducers: (state, action) -> newState, immutable updates, and one other item I've said before but I'm not remembering at the moment
  • Emphasis that combineReducers is simply a utility function for the common use case of delegating update logic based on state domain organization, and not a requirement
  • That defining object keys when using createReducers is, effectively, defining the name/shape of your state (not always clear when using ES6 object literal shorthand - naming of imported reducer functions matters!)
  • That "actions are passed to all reducers" only if you're using combineReducers
  • Passing different chunks or all of the state to a sub-reducer based on need for that action
  • Initializing state
  • Normalizing data
  • Updating normalized data
  • Updating data immutably in general, particularly nested data and arrays, and how just using a variable assignment doesn't mean you've "made a copy"
  • Reusing logic and creating reducers that can be "targeted"
  • A whole bunch of other stuff that has crossed my mind at various points and that I'm not thinking of right now, but hopefully will remember later

Random related links:

Some overlap with the performance/optimization discussions in #1783 , the store organization mention in http://redux.js.org/docs/FAQ.html#organizing-state-nested-data , and the concepts in http://redux.js.org/docs/recipes/ComputingDerivedData.html .

@blvdmitry
Copy link

This may help a bit https://github.com/nosovsh/reduceless

@markerikson
Copy link
Contributor Author

@bananabobby : thanks for the link, but I'm looking to document and clarify idiomatic Redux usage.

@markerikson
Copy link
Contributor Author

Reactiflux quote:

[8:33 PM] Francois Ward: where state lives is more a matter of how you plan on manipulating it than anything else.
[8:34 PM] Francois Ward: so if you have, let say a "loading: true/false" flag that -always- updates when you set some entity, it probably should live there.
[8:34 PM] Francois Ward: if its completely independant, changes separately, etc, then it should be its own thing.
[8:34 PM] Francois Ward: as you develop apps you often will refactor that stuff.
[8:35 PM] Francois Ward: since the relationship between actions, components and reducers is completely loose and maleable, it can change all the time.

@gaearon
Copy link
Contributor

gaearon commented Jun 10, 2016

@axelboc
Copy link

axelboc commented Jun 24, 2016

I could use some advice around using combineReducers when some slices of the state are static and don't need to be reduced. Here is a contrived example:

// This is what I'd like the state to look like:
{
  isUserSignedIn: true, // static, hydrated data - doesn't need a reducer
  todoTemplates: [...], // same
  todos: [...] // hydrated then manipulated - needs a reducer
}

// This is one way of making it work:
const reducer = combineReducers({
  isUserSignedIn: (state = false) => state, // basically a noop reducer
  todoTemplates: (state = []) => state, // same
  todos: todosReducer
});

The solution above is not very practical and scalable when the number of static slices of the state increases. I can think of a couple of other ways to make this work, but neither really stands out as the right solution:

  • don't use combineReducer => not scalable
  • move all the static data under a common key, like static, and use a noop reducer on that => scalable, but static is kind of ugly and doesn't describe what the data actually is (and there may not be a name that does, since all the static sub-keys may not have anything in common).

@markerikson
Copy link
Contributor Author

Not really intended as a Q&A thread, but that's definitely an interesting question I haven't considered before.

My initial thought was that a static value like that shouldn't really be in Redux state, but given that it's something server-hydrated, I can see a point to it. I've actually got some static-ish data coming back in my host page (user full name, etc), and at the moment a couple of my components are just referencing window.theVariableFromTheServer, but that's an intriguing idea now that I think about it.

I would think that putting it under a staticData key with a noop reducer would be reasonable. Suppose the big question is how much of this type of data is there, and where is it coming from.

@naw
Copy link
Contributor

naw commented Jun 24, 2016

@axelboc here are a couple of options:

  1. Don't put static stuff in the store.
  2. Just write your own root reducer which keeps all keys of state, and then overrides them with whatever your combined reducers returns. In other words, static keys are kept, and dynamic keys can still use combineReducers
combinedReducers = combineReducers({
  todos: todosReducer
 })

rootReducer = function(state, action) {
  return Object.assign({}, state, combinedReducers(state, action));
}

@naw
Copy link
Contributor

naw commented Jun 24, 2016

Also see this issue: #1457

In particular, Dan's comment here: #1457 (comment)

@axelboc
Copy link

axelboc commented Jun 24, 2016

Wow, thanks! This definitely should go in the docs. I'm going with your second solution @naw, as dealing with static data the same way as dynamic data will lead to the cleanest code in my situation.

This custom root reducer is so straightforward, logical and convenient, that it comes a little as a surprise to me that it's not already a built-in feature of combineReducers (perhaps an opt-in feature). Anyway, I'm sure there are reasons why it's not, and this is definitely not the thread for it... So thanks for your help!

@markerikson
Copy link
Contributor Author

Two thoughts:

  • putting "static" data in a Redux store is a relatively less common use case. After all, Dan's original article on "The Case for Flux" points out that a Flux-like approach is mostly useful for data that changes over time.
  • Per my comments up at the top of this thread: reducers are just functions, and combineReducers is just a utility function for the most common use case. Just like any other part of your application code, you can break things down into functions any way it makes sense to you, just that with reducers as a whole have to obey the basic rules of (state, action) -> newState and immutable updates. Certainly doesn't mean that every reducer function must have the exact (state, action) signature, though, just that the final result needs to be put together that way.

Glad that you got things working!

@naw
Copy link
Contributor

naw commented Jun 24, 2016

@markerikson Regarding the original intent of this issue you created --- I definitely agree a page about structuring reducers would be helpful.

I also strongly agree with your statement:

Emphasis that combineReducers is simply a utility function for the common use case of delegating update logic based on state domain organization, and not a requirement

Personally I think one thing that might help is standardizing terminology regarding things like "root reducer", "sub-reducers", "reducers", "helper functions", "reducer factories", "state", "slice", etc. In some cases we use the same terms to refer to different things, and other case we use different terms to refer to the same thing.

For example, what should we call the functions that manage a particular slice of state? Are they "reducers", "sub-reducers", both, neither? Is a function considered a reducer merely because it "helps" the rootReducer manage state in some way, or is it only a reducer if it has a certain method signature?

Moving on to combineReducers in general:

I think you could make the argument that combineReducers isn't really "core" redux. It's a high-level organization pattern that plugs in to the main redux engine (single store with dispatcher and subscriptions). There are other patterns that arguably work just as well, and as you've pointed out, all that matters is that the rootReducer itself exposes the correct method signature.

We do need to help people understand that combineReducers is just a pattern, and perhaps wrap some standard terminology around this pattern as well as other patterns, so that we have a language for the ecosystem as a whole. For example, if a 3rd party provides a "sub-reducer", it needs a way to communicate how it expects to be embedded into the rootReducer. Is it going to be embedded via combineReducers within a given key/slice, or is it going to be embedded at the top-level, or is it something else?

It's easy for 3rd party tools just to "assume" everyone is using combineReducers, even though combineReducers is not required. Sometimes it's feasible for a 3rd party tool to accommodate more than one pattern at once. For example, https://github.com/reactjs/react-router-redux can accommodate combineReducers and reduce-reducers : reactjs/react-router-redux#183

Maybe it would be helpful to highlight some of these other patterns alongside combineReducers to make it more obvious that combineReducers is not required?

Just trying to brainstorm with you...

@markerikson
Copy link
Contributor Author

@naw : excellent comments, and definitely some of the stuff that's going through my head. Not sure I have specific or definitive answers at the moment, but please keep tossing out those kinds of ideas for discussion.

@markerikson
Copy link
Contributor Author

Okay. I am finally, finally sitting down to start work on this. To be honest, there's so many related topics involved here that I'm not exactly sure how to address things.

Initial empty doc page pushed to https://github.com/markerikson/redux/blob/structuring-reducers-page/docs/recipes/StructuringReducers.md . I'll try to update it as I go. Suggestions and feedback wanted!

@markerikson
Copy link
Contributor Author

markerikson commented Jul 5, 2016

I was able to get started on this and crank out a decent first chunk of work. Unfortunately, it basically duplicates a large portion of what's already in the "Reducers" docs page, and Dan's videos.

There's a couple reasons for that. First, I feel like I need to be covering things from first principles. Stuff like what "mutations" and "immutability" are, how to update data immutably, demoing how to refactor a reducer into smaller functions, etc.

I'm probably going to take a suggestion from @naw and prefix this with a page saying "Go read articles X, Y, and Z first, and make sure you totally understand them." (Loosely, "you must be THIS tall to ride".) For immutability, http://reactkungfu.com/2015/08/pros-and-cons-of-using-immutability-with-react-js/ is fantastic, http://wecodetheweb.com/2016/02/12/immutable-javascript-using-es6-and-beyond/ and http://t4d.io/javascript-and-immutability/ are pretty good.

Anyway, I'm also figuring on breaking this up into subpages. Rough sketch:

  • Intro concept and reducer rules
    • Prerequisite articles
    • Basic reducer structure and state shape
    • Splitting up reducers (terms and concepts)
    • Splitting up reducers (progressive refactoring example)
    • Using combineReducers
    • Using something OTHER than combineReducers
    • Normalizing state shape
    • Writing reducers for normalized data
    • Targeting updates and reusable/shared/duplicate logic

And, uh, whatever else I come up with.

@markerikson
Copy link
Contributor Author

One other side thought that I'm jotting down here for reference, but won't create an issue for yet: might be useful to have an "Idiomatic Redux Architecture" page or something, that talks about stuff like using mapDispatch and binding to keep your components "unaware" of Redux, etc.

@msageryd
Copy link

msageryd commented Jul 5, 2016

Thank you for your effort to educate the community. It's really great to have access to so much material while trying to learn new stuff. I have read through most of the articles/posts that you have written and linked to.

I'm completely new to Redux and I'd really like to read some best practice on what to store in the store. Maybe such information would fit in your new document? I might be way off here or come out as a complete fool, but I'll try to explain what I'm having a hard time to grasp by describing the flow in a new project I'm working on.

  • Serverside is Node.js and Postgres.
  • REST-API for now, maybe websockets later.
  • App must be usable without network, i.e. syncronization of data needs to be done both ways (new data in app goes to server, new data on server goes to app when network gets available again)
  • Local storage in app not decided upon yet (SQLite, AsyncStore, Realm, or whatever)
  • Local storage in app needs to be encrypted
  • Locally cached data might get withdrawn upon synchronization if the users access rights are changed for some objects on the server.
  • Some data will be described with JSON Schema for dynamic form layout. The schemas will be fetched from the server as well. Would these schemas go into the store as well? They are completely static once they are fetched.

What data goes where? I think I'd like the local database to be the truth. After some usage time this database will hold most of the relevant data fetched from the server as an offline cache. After a while I will throw out old data though.

I assume that Redux will serve as kind of a secondary cache? I.e. it won't hold data that is not loaded/used in this app session, would it? Or should Redux hold all data? If that is the case the local database would be an exact copy of the Redux store and the Redux store would potentially be quite big.

I have a lot of image handling in the app. Images will be referenced from the objects in the Redux store by UUID (also a key in Postgres DB). Local disk on the device serves as an image cache with UUIDs as filenames. I shouldn't cache binary data in the Redux store should I? Or is there a balance somewhere? Maybe Base64 encoded thumbnails can live in the store? But not 5 Mb jpg files, for sure..

In what order should I fetch data?
A. Container needs props. Props fetched from local DB if available, otherwise from REST API. After fetching, the data gets duplicated in local db and Redux store.

or B. Container needs props. Props fetched from local DAO which handles local DB as cache

And what about writing local changes to local database for later synchronization with server? Should I have a DAO which subscribes to store changes and saves changes to database. And at the same time have a synchronizer subscribing to changes for sending to the server if network is available?

I'm having a hard time to envision how Redux fits in with a local database. Hopefully I'm overthinking stuff and instead there is a really simple solution to all this =)


And some thoughts about immutability (a concept quite new to me as well)..
If I understand correctly I could make a shallow copy of the state in the reducer in order to save on the time it would take to make a deep copy? This would be done only to get a new object reference and fool the data consumers that I have provided a shiny new object. But in fact I have the same old referenced data deeper down in the object. Is this really immutability? I'm perfectly fine with whatever, but I would really like to read some more on this. How much of a state-slice should be immutable for the state to be considered immutable? I might have misunderstood something here though...

Btw, thank you very much for your answer at SO about store structure.

@msageryd
Copy link

msageryd commented Jul 6, 2016

After some sleep I now see that I might have hijacked your thread with my own questions. Please take the above questions as input and suggestion on what to include in your StructuringReducers.

@markerikson
Copy link
Contributor Author

Yeah, bit of a digression there. You might want to drop into the Reactiflux chat channels and ask some of the questions there.

For immutability, the key is that you never modify the contents of an existing object reference. If you have an = sign for an assignment, and the thing on the left side of the = isn't already a copy, you're probably directly mutating data. So yes, that means making a shallow copy, and overwriting some of the fields in that copied object. See the list of articles I have linked for this topic at https://github.com/markerikson/react-redux-links/blob/master/immutable-data.md. You probably also want to read through the Redux FAQ, which includes topics like deep-cloning vs shallow-cloning: http://redux.js.org/docs/FAQ.html#performance-clone-state

@markerikson
Copy link
Contributor Author

While I don't want to go over the entire concept of updating data immutably, it would probably be useful to add a page that demonstrates various recipes and approaches for specific tasks. For example, updating a nested object field using Object.assign vs object spreads vs a couple immutable update utilities vs a Ramda lens.

@markerikson
Copy link
Contributor Author

Another related topic: "thin" reducers vs "thick" reducers, per http://redux.js.org/docs/FAQ.html#structure-business-logic . I very much tend towards 'thick" reducers myself, but I've seen a number of utilities and libs that treat part or all of a Redux store as a simple key/value storage, with reducer logic along the lines of return {...state, ...action.payload}. I know @Phoenixmatrix seems to prefer that sort of approach. That obviously changes reducer logic structure considerably.

@markerikson
Copy link
Contributor Author

Immutable data update tools:

  • utilities like dot-prop-immutable and React Update Addons
  • common utilities like Lodash (in "FP" mode) and Ramda
  • Redux-ORM

@markerikson
Copy link
Contributor Author

Was able to crank out first drafts for "Using combineReducers" and "Beyond combineReducers" yesterday. Looks like the next couple topics on my list are dealing with normalized data.

@markerikson
Copy link
Contributor Author

I should also include something about the approach Dan uses in his first video series, where he defines a reducer function for a single todo, and then reuses it in a couple different contexts to do updates ( https://github.com/tayiorbeii/egghead.io_redux_course_notes/blob/master/08-Reducer_Composition_with_Arrays.md )

@mmazzarolo
Copy link

mmazzarolo commented Aug 28, 2016

@markerikson this thread is really informative, thanks for all the links

@markerikson
Copy link
Contributor Author

@mmazzarolo : Thanks. If you have any specific feedback about the current WIP versions of the doc pages, I'd definitely like to hear it.

@davincho
Copy link

davincho commented Aug 30, 2016

Thanks @markerikson for this awesome overview of structuring reducers! Looking forward to reading https://github.com/markerikson/redux/blob/structuring-reducers-page/docs/recipes/reducers/07-UpdatingNormalizedData.md ;)

@markerikson
Copy link
Contributor Author

@davincho : Thanks. If you've got any other comments or suggestions, please let me know.

Anything specific relating to normalized data that you'd like to have covered?

@msageryd
Copy link

@markerikson Maybe food for the "Updating normalized data" chapter:
http://stackoverflow.com/questions/39243075/redux-structure-for-image-capturing

I would be thrilled to hear if there is a best practice for this kind of "ownership" updates. I.e. which entity should be responsible for updating the relationship.

@jimbolla
Copy link
Contributor

In a "slice reducer", is it better to name the state arg state or something closer to what it is? In other words:

  • function todosReducers(state, action)
  • function todosReducer(todos, action)
  • function todosReducer(todosState, action)

@markerikson
Copy link
Contributor Author

Since it's just naming a local variable, totally up to you. I can see arguments both ways. Calling it state would be sticking with a consistent convention. Giving it a specific name could be easier to read for intent and context.

@markerikson
Copy link
Contributor Author

@MichaelSWE : Read your SO question, and I'm not sure I understand the context enough. Could you sketch out your current store structure for me?

Also, I'm not sure I understand the phrase "which entity should be responsible for updating the relationship". Entities are really just plain data - it's the reducer logic that's responsible for doing the updates.

@davincho
Copy link

@markerikson some input from me:

  • Best practice of sharing update logic of entities (Updating same type of entity but in different action creators)
  • Updating nested entities (normalized with normalizr). Probably a generic solution is possible as relationships between entities are known via Schema definition.

@msageryd
Copy link

msageryd commented Sep 2, 2016

@markerikson Thank you for taking the time for my SO question. "I should have written which reducer should be responsible.."

It almost boils down to a master/detail scenario, but it's a 1:1 relation so I need to update the foreign key of the master after creating the detail. A view is showing the master record and I need to create a detail record and link it to the master.

In the case when images are the "details" some master entity types needs to reference the image in different ways. Some examples:

project.coverImage
profile.avatar
profile.avatarThumbnail
controlpoint.imageBefore
controlpoint.imageAfter

So, after the image is saved to disk I need to instruct a reducer to populate the master record in the correct way. This can be done in many different ways, but I thought that there might exist a best practice for this.

  1. The "master view" could dispatch an action which pushes the cameraView onto the navigation stack and populates the props with both its own id (master id) and the id of the future detail record. This could be done in a thunk which also dispatches the needed information to update the master record with the detail ids. I would have to handle the case where the user closes the cameraView without saving an image and reset the optimistically set detail record in the master.
  2. The master view could dispatch an action which pushes the cameraView and includes all the needed instructions for a reducer to update the master entity after the fact that an image is saved.

I'm hoping that I managed to explain myself better this time.

@markerikson
Copy link
Contributor Author

Definitely need to address initializing state. Probably best if I just copy Dan's answer in http://stackoverflow.com/questions/33749759/read-stores-initial-state-in-redux-reducer/33791942 .

@markerikson
Copy link
Contributor Author

"Multiple instanced data" issue/discussion roundup: #1602 (comment)

@markerikson
Copy link
Contributor Author

Yay. I have FINALLY managed to crank out "Managing Normalized Data". I also went ahead and copied Dan's answer on "Initializing State" from Stack Overflow into a separate subpage and updated the formatting.

With that, I think I have completed all the major content that I wanted to cover for this effort. The next step is to get some actual eyes on this, and look at usefulness of content, organization, etc.

I have not yet tried to actually build the Gitbook stuff on this branch. I figured I'd wait until I was done content-wise to give that a shot.

@reactjs/redux , @jimbolla , @aweary , @naw , @Phoenixmatrix , @tommikaikkonen , et al: I would greatly appreciate any and all feedback on the stuff I've put together here. I'm definitely figuring that the order of the topics will need to be shuffled a bit. Also, the current link to "Prerequisite Concepts" on the TOC page probably needs to be reformatted better. Beyond that... thoughts? Comments? Suggestions?

@timdorr
Copy link
Member

timdorr commented Sep 4, 2016

Can you make a PR? That will let us comment on stuff line-by-line, rather than on the document as a whole.

@markerikson
Copy link
Contributor Author

@timdorr : Done. See #1930 .

@markerikson
Copy link
Contributor Author

Okay, so we've got the primary content merged in. The TOC file needs to be updated to include the new pages, and I'm wanting to do some renaming as well.

As a side note, the docs seem to have some odd numbering going on. They should be numbered "1, 2, 3", but instead it's "1.1, 1.2, 1.3", etc. Looking at some other Gitbook examples and can't see a real difference in the TOC definition.

@timdorr
Copy link
Member

timdorr commented Sep 27, 2016

This is merged! Yay!

@timdorr timdorr closed this as completed Sep 27, 2016
@markerikson
Copy link
Contributor Author

W00t!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

10 participants