-
-
Notifications
You must be signed in to change notification settings - Fork 15.3k
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
Redux and immutable stores #548
Comments
Redux makes no assumptions about the type of state you return from the reducer. You can use plain objects: function reducer(state = { counter: 0 }, action) {
switch (action.type) {
case: 'INCREMENT':
return { counter: state.counter + 1 };
case: 'DECREMENT':
return { counter: state.counter - 1 };
default:
return state;
}
} immutable data structures: function reducer(state = Immutable.Map({ counter: 0 }), action) {
switch (action.type) {
case: 'INCREMENT':
return state.update('counter', n => n + 1);
case: 'DECREMENT':
return state.update('counter', n => n - 1);
default:
return state;
}
} or any other value type, like a number: function reducer(state = 0, action) {
switch (action.type) {
case: 'INCREMENT':
return state + 1;
case: 'DECREMENT':
return state - 1;
default:
return state;
}
}
In fact, that's exactly why immutable data and Redux go so well together. If you use persistent data structures, you can avoid excessive copying. You may have misunderstood because of the way selectors work in react-redux. Because the result of a selector is passed to React's function select(state) {
return {
counter: state.get('counter')
};
} Hope that makes sense! |
This is a good topic for the docs |
Thanks for the explanation. Seems like there are a few other gotchas like Issue #153. Would be great to have a place in the docs for the proper way to take advantage of an immutable store. There are also projects like Seems like everyone is touting the benefits of immutable stores, but in this case the path of least resistance is to just use plain objects instead. |
#153 shouldn't be an issue. If you want the entire state tree to be an immutable object (rather than just the values returned by sub-reducers), it does require a bit more work, but nothing prohibitively difficult. You need to either use a specialized form of function combineImmutableReducers(reducers) {
// shallow converts plain-object to map
return Immutable.Map(combineReducers(reducers));
} or eschew As for the specific projects you mentioned, redux-devtools will work just fine regardless of the type of state. redux-react-router currently expects the router state to be accessible at |
Ah, so that's what |
Didn't mean to close if you are using this issue for docs. |
👍 for adding a section in the docs for this - or maybe create a specific repository with all the instructions about how to use Redux with Immutable.js, and link it to the docs. We also decided to use immutable.js, and for now what we did was to create a (very simple) custom combineReducers, didn't think about using the default one and then just transform it to Immutable.Map() Great suggestion @acdlite :) |
Am I right to say that if you always return a new state in the reducer and therefore you don't mutate the state, it makes no difference to an immutable data structure performance wise? Or are there some other benefits beside less error prone? |
@ms88privat the performance advantage of using an immutable state is in the rendering of React components: with immutable state, you can compare references to learn if the state has changed inside shouldComponentUpdate, without any complicated (and potentially slow) deep comparisons. |
@ms88privat as @acdlite said in this comment, the improvement of using immutable data structures over plain js objects and arrays is the ability to reuse the unchanged things as much as possible, and avoiding excessive copying. By assuming the data is immutable, the library can do structural reusing of the collection, and share the unchanged parts between different objects, and still making it look to the outside as completely different structures. @danmaz74 the performance improvement you mentioned also applies to using plain objects or arrays without mutating (ie creating a new state with the changes). The difference between something like |
@leoasis Thats what i wanted to hear. So react performance wise it should be the same, but it has less copying / memory-footprint or something like that. What is the default status of shouldComponentUpdate with Redux? I read something about pureRenderMixing ... which i think should apply to redux, like we said. But how to implement in ES6? |
@ms88privat As per your last question regarding ES6 and |
@leoasis thx, makes sense - so it's good to go. |
@leoasis: @ms88privat wrote: "If you always return a new state in the reducer and therefore you don't mutate the state, it makes no difference to an immutable data structure performance wise". That's not correct, because if you always return a new state in the reducer (ie, also when there is no change) you can't just compare the reference - the comparison will always return the "new" state is different from the "old" state. That is in addition to the fact that using Immutable to create a new changed state is faster than creating a new deep copy. |
@danmaz74 Well, if you do something like this: return {...state, something: 'changed'} then the root state and the It is true though that the Immutable.js library checks if the value you are setting is already set, and does nothing if that's the case, but that's fairly simple to do with POJOs as well: return state.something === 'changed' ? state : {...state, something: 'changed'} |
@danmaz74 Ok, i have to correct my spelling. If there is no change at all (= no action), the reducer will return the last state and there is no need for a new state. |
@ms88privat ok no problem, I just interpreted that literally. But in using Immutable (or something similar), there's still the advantage of easily getting what @leoasis pointed out in his last comment - without a library you can still do it using using ES7 spreads, but it looks much more complicated/less maintainable that way (at least to me). |
We need “Usage with Immutable” in docs Recipes folder. |
@gaearon I agree. I started playing with redux over the weekend trying to port a current app over from a flux implementation. I've been slowly figuring out how to use immutable but hit snags in the reducers and the tests for them. I've been able to use chai-immutable for my tests (https://github.com/astorije/chai-immutable) but it took some bumbling around by me to figure it out. That being said, it hasn't been a terrible burden to figure out. It would just be nice to have a smoother onramp. |
@acdlite after a deeper analysis, unfortunately you can't simply do let combineImmutableReducers = reducers => {
var combined_reducers = combineReducers(reducers);
return (state,action) => Immutable.Map(combined_reducers(
Immutable.Map.isMap(state) ? state.toObject() : state,action
));
} |
@danmaz74 Yes you're right, my mistake. It was meant to be a naive example; e.g. even with your fix, it returns a new Immutable map every time, regardless of whether any of the keys have changed. A more complete implementation would update the previous state map rather than creating a new map. I think using |
@acdlite You're right about the new map every time, Before testing the code above, we used this one: let app_reducers = (state=Immutable.Map({}), action={}) => {
for (let reducer_name of Object.keys(reducers)) {
state = state.set(reducer_name, reducers[reducer_name](state.get(reducer_name), action));
}
return state;
}; This wouldn't change the root state if nothing changes below, but it wouldn't do the checks that combineReducers does. Honestly it's not clear to me which approach would be better; I guess that changing the root map would usually do very little damage, but of course it wouldn't be 100% clean. Any thoughts? |
I also would love more documentation/examples on using immutable.js with Redux. Also, has anyone used normalizr with immutable.js? I was thinking of using it in some api middleware and was wondering if it was viable. |
@dfcarpenter Normalizr is mostly useful for normalizing API responses before they become actions, so there's no difference: you can use it regardless of what you use for state. |
@dfcarpenter We do use normalizr with redux, I have a createEntityReducer factory : import Immutable, { Map } from "immutable";
export default function createReducer(reducer, entitiesName) {
return (state = new Map(), action) => {
const response = action.response;
const data = response && response.data;
const entities = data && data.entities;
const collection = entities && entities[entitiesName];
if (collection) {
return reducer(state.merge(Immutable.fromJS(collection)), action);
} else {
return reducer(state, action);
}
};
} I realize now that I could probably improve it by making it a middleware to which I would pass all my schemas.. To not be too much off topic, I'm also really interested in having redux working with Immutable from the grounds up. I like |
@chollier I was wondering if you can confirm if I understand correctly about using both normalizr and Immutable.js with redux. I am just starting to get deeper into React/Flux, but it seems like the benefit of Immutable.js with React is to reduce graph traversal through something like the PureRenderMixin. If your store stores each model independently/unlinked using normalizr with references through ids to other models, it seems like there is little opportunity to know if the subgraph needs rendering (for example, maybe only at the leaves) since Immutable.js will not actually store the graph, but the individual links so the simple immutable instance checks will not cover the changes lower in the graph. Another way to ask this is: are there any common ways to optimize React rendering using normalizr or do you have to build the full virtual DOM whenever changes occur in a hierarchical model graph? |
One small potential gotcha that you might encounter when using normalizr, immutable.js and numeric IDs... If you have a collection: normalizr will generate something like: If you pass normalizr's output to This can lead to unexpected bugs, for example you might have seemingly harmless code like: const user = userCollection.get('1')
const account = accountCollection.get(user.get('account')) But that won't work, and account will be undefined. |
@kmalakoff I think one answer to that is to use cursors. Also because of the way immutable.js store things, even if part of your tree change, you can still get equality comparison on other parts of the tree. Example :
|
interesting topic. Would like to see more examples. |
Yeah,need more examples for immutable and normalizr |
@chollier makes sense. From what I understand with normalizr and your example, if the List in 'f' referred to related models in the store, you would only be able to check if the reference to the ids changed, but not is the underlying models changed (since they are not embedded in 'f') so you wouldn't know if the tree needs rendering and would need to render it regardless of whether the ids changed, right? |
Closing, as there doesn't appear anything actionable for us here. If somebody wants to write “Usage with Immutable” please do! Unfortunately I don't have experience in using them together so I can't help. |
More references: Plain Redux with ImmutableJs: A project that combines Redux with Seamless-Immutable: |
Although this was closed back in September, I still haven't found any clear examples of using Redux with ImmutableJS, especially when it comes to Demo I think I was initially hitting the same hurdle that @kmalakoff was touching on. That with the normalised data, how does the container record "know" to update when one of it's children update - as it's just a sequence of numbers/strings/ids. In the example above, I have created the data in a normalised way, but could also be achieved using Dan's nifty normaliser library. Notes
I feel like this is the right approach, but am still learning redux/react/immutable, so would love to get anyones feedback. It's not a recipe as @gaearon mentioned, but hopefully this is on the right track. |
I'm starting to wrap my head around Redux and was previously using a basic flux architecture based around immutable stores. It seems like getting Redux to work nicely with an immutable store is a bit of work with lots of gotchas (from connect, to reducers, to selectors, to the dev tools, etc).
Since changes always flow through reducers and reduces shouldn't mutate state, are most people just going without an immutable store when they switch over to Redux? I'm concerned that this will make
shouldComponentUpdate
harder to write since you can't immediately tell what state has changed using strict equality which will lead to lots of unnecessary renders.Was there a reason that Redux wasn't built with an immutable store?
The text was updated successfully, but these errors were encountered: