What you are talking about here is a reducer which is made by combining multiple "slice" reducers through combineReducers. Something like this:
const rootReducer = combineReducers({
users: usersReducer,
posts: postsReducer,
})
Let's say that we dispatch an action with type ADD_USER -- this is something that usersReducer will respond to but the postsReducer won't.
Each reducer returns the state - does each reducer return a new reference to the state even in case of default return state?
The postsReducer doesn't care about this action and will just return state. This is a reference to the existing object. So state.posts is identical.
The usersReducer will create and return a new object for its state. So state.users is a reference to a different object in memory than it was before. If there are other arrays or objects nested inside of state.user then those objects will be the same references unless we replaced them in the reducer. So you are dealing with a "shallow copy" where state.users is a new object, but a deeply nested object like state.users.byId[5] might be the same exact object as before.
You might think that the root state would always be a new object reference when dealing with combineReducers since it is essentially doing this:
const rootReducer = (state, action) => ({
users: usersReducer(state, action),
posts: postsReducer(state, action),
});
But the actual implementation is more complex and it involves some checks to prevent unnecessary object creation. If every individual reducer just uses default: return state; then the original state object is returned.
Here's how the combineReducers function works:
- It has a
boolean flag property hasChanged which is initially false.
- It creates a
nextState which is initially an empty object (so obviously that's a new object).
- It goes through each of the individual property reducers and updates the
nextState with the updated state for that property. If the property reducer returned a new state then hasChanged becomes true.
- If
hasChanged is still false after all the property reducers have been called, then it ignores the nextState and instead returns the original state variable.
- Otherwise it returns the
nextState.
When using the React bindings like useSelector, it (by default) does a strict equality check on the returned value. So if you are returning a new object which is an exact copy of the previous object, it will trigger a re-render because the object is a different reference. This is why it is important to use something like reselect to memoize your selectors if they involve creating an object through array.map, array.filter, etc. Otherwise you will have a new object reference every single time. useSelector also supports custom equality checks so that you have control over when re-renders occur.