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.
Related
I'm trying to figure out how to set my React component's state based on the previous state, while also using the Immer library to allow for easier immutable state changes.
Normally, without Immer, I'd safely access the component's previous state using the updater function version of setState():
this.setState(prevState => {
prevState.counter = prevState.counter + 1;
});
However, when using the Immer library to allow for easier immutable state changes, you no longer have access to the updater function, or the prevState variable. This is fine for most operations, as the Immer draft proxy can be used for simple changes - but it doesn't work for more complex changes such as comparisons, as the proxy doesn't reflect the original state object:
this.setState(produce(draft => {
const index = draft.list.indexOf(item); // doesn't work - always returns -1
if (index > -1) {
draftState.list.splice(index, 1);
}
});
The problem is that since the draft state is a proxy, comparisons such as indexOf always fail, since the two objects are inherently different. But I don't want to just use this.state in my produce function, as the React docs are very clear that you shouldn't rely on its value when calculating the new state, and I don't want to do the state update without Immer, as complex state object changes are significantly simpler when working with a mutable proxy object.
Is there any way to access an up to date version of the component's previous state, while still using the Immer produce function in my setState calls?
Edit:
Updating to add a (simplified) example of my actual code:
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
this.state = {
fooBar: {
selectedOptions: []
}
};
}
handleClick(clickedOption) {
/*
if clickedOption is already selected
remove option from selectedOptions
else
add option to selectedOptions
*/
this.setState(produce(draft => {
const index = draft.fooBar.selectedOptions.indexOf(clickedOption); // The problem is here
if (index > -1) {
draft.fooBar.selectedOptions.splice(index, 1);
}
else {
draft.fooBar.selectedOptions.push(clickedOption);
}
}));
}
The problem is at the const index = ... line. If I use draft, then the indexOf function always returns -1, and if I use this.state, then I'm not guaranteed an up-to-date version of selectedOptions to compare against...
You could implement a usePrevious() hook yourself to store any previous variable:
function usePrevious(value) {
const ref = useRef();
// Store current value in ref
useEffect(() => {
ref.current = value;
}, [value]); // Only re-run if value changes
// Return previous value (happens before update in useEffect above)
return ref.current;
}
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 thought that context acts in a similar way as state so I've created function handleUpdate that can update state which is context using as a value. Afterwards I've noticed that context is being updated without triggering handleUpdate.
Provider:
<DashboardContext.Provider value={{dashboard:this.state,handleChange:this.handleChange}}>
{/*...*/}
</DashboardContext.Provider>
handleChange function
handleChange=(what, value)=> this.setState({[what]:value});
In another component which uses context: this triggers updating of context without calling handleUpdate.
let updatedTasks = this.context.dashboard.tasks;
updatedTasks[task.id] = {/* ... something */};
Afterwards it changes context value and parents state (which is context using) without calling setState. Is this usual behavior? I though that all states should be handled with setState function.
As the actual workaround to lose reference on contexts object I've used:
let updatedTasks = JSON.parse(JSON.stringify(this.context.dashboard.tasks));
but it doesn't seems like a correct solution for me.
Edit: as #Nicholas Tower suggested solution:
my current code
State in constructor now looks like this:
this.state = {
value: {
dashboard: {
// everything from state is now here
},
handleChange: this.handleChange,
}
};
I pass state.value instead of custom object now
<DashboardContext.Provider value={this.state.value}>
{/*...*/}
</DashboardContext.Provider>
but still when I do this, context and state (both) are being updated without calling handleChange
let updatedTasks = this.context.dashboard.tasks;
updatedTasks[task.id] = {/* ... something */};
The issue you have is in this part:
value={{dashboard:this.state,handleChange:this.handleChange}}
Every time your component renders (for whatever reason), this line will create a new object. The dashboard property and handleChange property may be unchanged, but the object surrounding them is always new. That's enough that every time it renders, the value changes, and so all descendants that use the value need to be rerendered too.
You'll need to modify your code so that this object reference does not change unless you want it to. This is typically done by putting the object into the component's state.
class Example {
handleChange = (what, value) => {
this.setState(oldState => ({
value: {
... oldState.value,
[what]:value
}
});
}
state = {
value: {
dashboard: {
// whatever you used to put in state now goes here
},
handleChange: this.handleChange
}
}
render() {
return (
<DashboardContext.Provider value={this.state.value}>
{/*...*/}
</DashboardContext.Provider>
)
}
}
You can see mention of this in React's documentation on context: https://reactjs.org/docs/context.html#caveats
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.
The below object is action.data has a nested object address
{
name: 'Ben',
address: {
country: 'Australia',
state: 'NSW'
}
}
How should I handle it in the reducer?
const rootReducer = (state = initState, action) {
switch(action.type) {
switch RECEIVE_DATA:
return {...state, data: action.data}
}
}
Can I do it as above? that I just assign the whole object to data without copying?
or
const rootReducer = (state = initState, action) {
switch(action.type) {
switch RECEIVE_DATA:
const address = {...action.data.address}
const data = {...action.data, address}
return {...state, data}
}
}
Or should I do a deep copy of the object and assign it to data?
thanks
The "correct" way to handle updates of nested data is with multiple shallow copies, one for each level of nesting. It's also certainly okay to create a new object that just replaces one field completely, per your first example.
See the Redux docs section on Immutable Update Patterns for some info on how to properly do immutable updates, and also the Redux FAQ entry regarding deep cloning.
From Redux:
Common Redux misconception: you need to deeply clone the state. Reality: if something inside doesn't change, keep its reference the same!
No, shallow copy of Unchanged properties.Changed properties will be anyway new values, so no question of type of copy.
In code we achieve it like this return {...prevState}
If you are changing only one item in an array, Redux docs says you can use array.map, but if you know the index, this is faster:
state[action.args.index] = {
...state[action.args.index],
disregardLeafNode: action.args.checked
}
return state