Immutability in react redux reducer - reactjs

If I write my reducer in the following way, then the "render" method is getting called and its a expected behavior. No problem here :
const initState = {
entries: ["test1"]
};
export const EntryReducer = (state = initState, action) => {
switch (action.type) {
case ADD_ENTRY:
return Object.assign({}, state, {
entries: state.entries.concat("test2")
});
break;
case DELETE_ENTRY:
break;
}
return state;
}
But, if I write the reducer in the following way, then the "render" method is not getting called though state is being updated :
export const EntryReducer = (state = initState, action) => {
let newState = Object.assign({}, state);
switch (action.type) {
case ADD_ENTRY:
newState.entries.push("test2");
break;
case DELETE_ENTRY:
break;
}
return newState;
}
I could not understand why render is not getting called. As far as I understand, "newState" is a immutable object and does not hold any reference to the "state" object.
Please help me understand it. Thanks.

Because Object.assign is shallow, it will not create a new Array, the old one will be mutated by .push():
state.entries === newState.entries // ["test1", "test2"]

"newState" is a immutable object
If you don't do that on your own, it's ordinary object/array/etc so it is not immutable.
why render is not getting called
React-redux tries its best and actually wraps your component into PureComponent. It does that to make all your connect()-ed components will not re-render on any action called but only once store has been updated in relevant places.
To realize if relevant data has been changed or not, PureComponent compares shallowly like oldDataProp !== newDataProp. Once you mutate objects/arrays that check will fail so component will not re-render.

It has nothing to do with the way you update the state. Consider it correct, though I would not recommend to avoid common best practices.
In your case render is not called because component props is not changed...
I believe that you may have something like this:
<Component entries={this.props.entries}/>
and
mapStateToProps = (state) => ({
entries: state.entries
})
If it is so, then state.entries is the prop that controls whether your component will be re-rendered or not. If it has the same value during the state change on ADD_ENTRY action - the component will not be re-rendered.
So. Get back to roots. Remember that in JavaScript state.entries is a POINTER, that points to array in memory.
While you calling entries.push the array in memory will be extended with another element - thats for sure. BUT the POINTER value that we have in state.entries will remain the same. It will be not changed. Thats how Array.push works.
As the result <Component entries={this.props.entries}/> will not be re-rendered.
By changing newState.entries.push("test2");
to
newState.entries = newState.entries.concat("test2");
you actually changing entries pointer, component sees that property props.entries has changed and now the whole thing re-renders...

You need to use other alternatives because Object.assign() copies property values. If the source value is a reference to an object, it only copies that reference value.
You should use a Deep clone.
let newState= JSON.parse(JSON.stringify(state));

Related

Implementing reselect in redux prevents new changes to appear instantly

In my React project, I have implemented memoization using reselect library.
The state basically has a list of objects which I render as cards.
Before implementing reselect, whenever I added a new element, the change instantly showed up and a new card got added at the end. However, now when I add a new element it does not instantly shows up, but rather shows up when the page is reloaded.
Why does this happen? And is there a way to fix this without removing the use of reselect library
EDIT : The issue has been solved, and as pointed out in the answers it was because I was simply mutating the state
The earlier code was as follows
case IssueActionTypes.ADD_ISSUE:
state.issueList.push(action.payload)
return {
...state
}
which I replaced with
case IssueActionTypes.ADD_ISSUE:
return {
...state,
issueList : [...state.issueList, action.payload]
}
which fixed the issue
Most likely you are returning mutated state in your reducers instead of returning a new array.
Docs:
createSelector uses an identity check (===) to detect that an input
has changed, so mutating an existing object will not trigger the
selector to recompute because mutating an object does not change its
identity. Note that if you are using Redux, mutating the state object
is almost certainly a mistake.
Example of returning mutated state (from docs):
export default function todos(state = initialState, action) {
switch (action.type) {
case COMPLETE_ALL:
const areAllMarked = state.every(todo => todo.completed)
// BAD: mutating an existing object
return state.map(todo => {
todo.completed = !areAllMarked
return todo
})
default:
return state
}
}

React redux-strange behavior, prevState and newState change together

I started to work on some frontend project, I needed to plant foundation and I choosed to go with react and redux. I went through some tutorials and started to build my project.
Everything was working fine, but all of sudden I m stuck with problem I'm not even sure how to describe it.
Problem is connected with using redux, reducers.
In short somehow my prevState was changing with the newState. The worst part is I cant share much more information, I'm new to this react/redux, so I'm not even sure if this is even possible.
Here is the example code:
export default function (state: UserReducerState = initialState, action: Action): any {
const newState = Object.assign({}, state);
console.log(state);
state.loginStatus.username="This change should only be on prevState";
console.log("newState",newState);
return newState;
};
When I console.log(newState) it has the change from the old state even though the newState was copied before the state.loginStatus.username ="This change......."
I dont know what code should I show more, but this is where problem start,I think.
How is even possible that object that is copied have the refrence to the old one ?
Issue
Even though you are copying the state object it isn't a deep copy. You are mutating the more deeply nested properties of your previous state object since the new state is simply holding a reference to them.
state.loginStatus.username = "This change should only be on prevState";
newState is a new object, but newState.loginStatus.username is reference back to the same object the previous state holds.
Solution
I'm not sure why you'd mutate your old state object in a reducer, but when updating the more deeply nested properties in the new state you must shallow copy any nested state that is being updated. Typically this is achieved via the spread syntax.
export default function (state: UserReducerState = initialState, action: Action): any {
const newState = {
...state,
loginStatus: {
...state.loginStatus,
username: "This change should only be on newState".
},
};
console.log(state);
console.log("newState", newState);
return newState;
};

Updating string in redux store

I am using redux with react. I have a key in store whose value is a string.
This is what is get in store.getState();
{appBgColor: "#FFF"}
I am updating the store like this.
store.dispatch( changeAppBgColor("#FC0") );
Reducer
const rootReducer = (state = initialState, action) => {
switch (action.type) {
case CHANGE_APP_BG:
return { ...state, appBgColor: action.payload};
default:
return state;
}
};
Everything works fine, the appBgColor get changed whenever a new dispatch happens.
Problem
I have read that to change a key in redux store you should use concat, slice or join to make the state immutable. So I doubt I am doing it wrong. Is this the correct way to do it?
For your use-case of updating the field appBgColor within your state, your reducer implementation is correct.
Note that when you return { ...state, appBgColor: action.payload}, you are not mutating the state, but in fact, creating a copy of the existing state, applying the change and returning it. This makes sure that the state is immutable, ie it is not directly modified.
You would only need to use functions like slice, concat etc when you are updating nested items within your state. For eg, when you need to remove an element from an array within your state, you would need to use slice as shown below.
const index = state.findIndex(a => a.id === action.id)
return [
...state.slice(0, index), ...state.slice(index + 1)
]

Update state in deep structure not re-rendering component

I just got started with React+Redux and I have a problem.
I know that I am never supposed to alter the old state in the reducer and I am not doing that.
However, when I change a variable like this in my reducer, my component is not re-rendering, even though I have mapStateToProps with state.coupons
// this deep copies everything
let newState = Object.assign({}, state);
newState.coupons[2].events[0].eventRows[0].alternatives[0].selected = true;
return newState;
What am I doing wrong?
EDIT:
I even tested to use newState = JSON.stringify(JSON.parse(oldState)) but with no success
EDIT:
This is my mapStateToProps function
const mapStateToProps = (state, ownProps) => ({
coupons: state.coupons,
currentDraw: state.currentDraw
});
You can find the solution in redux docs: http://redux.js.org/docs/recipes/reducers/ImmutableUpdatePatterns.html#updating-nested-objects
The key to updating nested data is that every level of nesting must be
copied and updated appropriately. This is often a difficult concept
for those learning Redux, and there are some specific problems that
frequently occur when trying to update nested objects. These lead to
accidental direct mutation, and should be avoided.
You can do this manually, something like:
function updateVeryNestedField(state, action) {
return {
....state,
first : {
...state.first,
second : {
...state.first.second,
[action.someId] : {
...state.first.second[action.someId],
fourth : action.someValue
}
}
}
}
}
In practice, it's better to use a helper library to do this. You can find a list of helper libraries at https://github.com/markerikson/redux-ecosystem-links/blob/master/immutable-data.md#immutable-update-utilities, I would personally recommend immutability-helper or just switching to immutable.js.

Why can't modify the nestState directly?

In redux docs I see this:
Common Mistake #1: New variables that point to the same objects
Defining a new variable does not create a new actual object - it only creates another reference to the same object. An example of this error would be:
function updateNestedState(state, action) {
let nestedState = state.nestedState;
// ERROR: this directly modifies the existing object reference - don't do this!
nestedState.nestedField = action.data;
return {
...state,
nestedState
};
}
This function does correctly return a shallow copy of the top-level state object, but because the nestedState variable was still pointing at the existing object, the state was directly mutated.
But we know that combineReducers just judge the state change by the top-level reference.See redux comBindReducer.js src:
let hasChanged = false
const nextState = {}
for (let i = 0; i < finalReducerKeys.length; i++) {
const key = finalReducerKeys[i]
const reducer = finalReducers[key]
const previousStateForKey = state[key]
const nextStateForKey = reducer(previousStateForKey, action)
if (typeof nextStateForKey === 'undefined') {
const errorMessage = getUndefinedStateErrorMessage(key, action)
throw new Error(errorMessage)
}
nextState[key] = nextStateForKey
hasChanged = hasChanged || nextStateForKey !== previousStateForKey
}
return hasChanged ? nextState : state
It just access every reducer and judge whether the state reference changed.
So back to the question,the retured object reference is changed,so the the hasChanged variable is true,and redux works fine.So I don't know what's the side effect of state was directly?
If you know,please tell me,thanks.
I don't think that the example is directly related to cobmineReducers. I believe that it attempts to present a caveat related to immutability: whenever a value inside a complex object changes, the entire hierarchy should indicate that. So if state.nestedState.nestedField changes, it means that state.nestedState has changed, and so did state. It should be enough to look at state or at nestedState to realize that nestedField has changed.
Developers who are new to this concept, may often think that assigning state.nestedState to a new variable and then changing it is fine, but it's not, because a === state.nestedState is still true.
combineReducers only cares if one of the slices has changed. If the rule above is followed, it will "work fine". But what happens if we look directly at state.nestedState? the reference is still the same, so it doesn't work anymore.
When it comes to React, connect works in the same way. It shallow compares the props, relying on the same rule: no need to deep compare, because the parent should indicate if the child has changed.
Back to your question: what are the side effects of changing the state directly?
Let's use your example:
// reducer.js
function reducer(state, action) {
let nestedState = state.nestedState;
nestedState.nestedField = action.data;
return {
...state,
nestedState
};
}
This will work fine:
connect(state => ({ state }))(MyComponent)
This will not:
connect(state => ({ nestedState: state.nestedState }))(MyComponent)
I hope it answers your question, or at least sheds some light on it.

Resources