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;
};
Related
Can I update the state based on other state variables (as shown below in setCurrentQuestion()), or will it cause unexpected issues?
I can think of two workarounds:
"external" helper function that calls a getCurrentQuestionIndex() selector passed into a getQuestionByIndex() selector which returns an IQuestion object that I pass into the setCurrentQuestion() reducer
Keep as is but use a draft safe selector instead of accessing the state object directly
const slice = createSlice({
name: 'blabla',
initialState: { questions: undefined, currentQuestion: undefined, noOfQuestions: 0, currentQuestionIndex: 0 } as QuizState,
reducers: {
setQuestions: (state, action: PayloadAction<IQuestion[]>) => {
state.questions = action.payload;
},
setCurrentQuestion: (state, action: PayloadAction<number>) => {
if (!state.questions) return
state.currentQuestion = state.questions[state.currentQuestionIndex]
}
},
state.currentQuestion should simply not exist in your state. You got state.questions as the source of truth, and state.currentQuestionIndex to remember which question is the current question. Anything further is not atomic and minimal anymore. It certainly works to have a state.currentQuestion which duplicates one of the items in state.questions, but then you end up with problems like this one. I'd recommend to add a selector getCurrentQuestion() that does the lookup and that can deal with state.questions being falsy. It also should never not be an array btw, it's easier to deal with state if the types don't change. I'd initialise it as an empty array, and then the selector could be:
const getCurrentQuestion = (state) => state.questions[state.currentQuestionIndex];
To solve what you had in mind, looking at a different part of the state before mutating it in the reducer, you could also use a thunk - they have getState() in the scope and can run conditional logic. So you could simply not dispatch setCurrentQuestion if state.questions is falsy. But a better solution imo is what I described above with the selector.
I have this reducer.
const userInitialState = {
users: [],
};
const users = (state = userInitialState, action) => {
if (action.type === "FETCH_USERS") {
return {
...state,
users: action.payload,
};
}
return state;
};
export default combineReducers({
users,
});
initially the users property is edmpty array,when the new results from the api call comes
for example response like
https://jsonplaceholder.typicode.com/users
is this the correct way for immutable way in redux store for my array inside ?
A proper immutable update is best described as "a nested shallow clone". You don't want to copy every value in a nested data structure - just the ones that need to be updated.
But yes, that looks correct.
A couple additional observations:
You should read through the post The Complete Guide to Immutability in React and Redux and the Redux docs page on Immutable Update Patterns to better understand how to do immutable updates correctly
But, you should really be using our official Redux Toolkit package, which uses Immer to let you write "mutating" update logic that is turned into safe and correct immutable updates.
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));
I am new to Redux and was learning how to work with it and trying to get how it works behind the scenes. But, I faced the confusion with why we need to keep state immutable in Redux, I admit, there has already been asked a question close to mine Why should objects in Redux be immutable? but in there I couldn't find clear explanation on why we need to keep state immutable. For example, I have this code which mutates the state on button click and the below code works pretty fine even if I mutate state:
const initialState = {
counter: 0
};
const reducer = (state = initialState, action) => {
if (action.type === "INCREMENT") {
return {
counter: state.counter + 1
};
}
return state;
};
export default reducer;
Please can you kindly explain why above code works fine even if I mutate state and why I need to make state immutable.
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.