Reputation: 229
Consider these two reducers:
const reducer = (state = initialState, action) => {
switch(action.type){
case 'changeName' :
return { ...state, name: action.name }
default:
return state
}
}
And:
const reducer = (state = initialState, action) => {
switch(action.type){
case 'changeName' :
state.name = action.name
return { ...state }
default:
return state
}
}
And let's say we have a react component with the following hooks:
const reduxState = useSelector(state=>state)
const [toggle,setToggle] = useState(false)
React.useEffect(() => {
const name = toggle?'Joe':'Bob'
dispatch({type: 'changeName', name}));
console.log('name is:', name, 'name in redux is:', reduxState.name)
}, [toggle]);
And let's say this is in a component that changes 'toggle' onClick. If we use the first reducer we get the following output:
name is: Joe name in redux is: Bob
or
name is: Bob name in redux is: Joe
This is because the component must rerender for us to get the updated value from redux. But, what if I needed the updated values from redux within the same useEffect Hook. I know this is a trivial example because I know the name before I set it in redux, but let's say we were fetching data from an api and I needed the updated value right away in the same hook. The second reducer seems to solve this problem. It prints:
name is: Joe name in redux is: Joe
or
name is: Bob name in redux is: Bob
However, it does this by updating the redux state directly before it emits a new object with the updated state. Redux documentation states that redux state should never be updated directly, but this is because it prevents components from being subscribed to changes in redux state (redux uses a shallow compare to see if components should be rerendered). Yet, in the second reducer, we are in fact emitting a completely new object, but we are just altering the state directly before so that the value is updated immediately.
Anyway, my question is, is this breaking any other rules I'm not aware of? Is there any wrong with implementing the second reducer? To me it seems like an upgrade because we get the updated value before and after the rerender.
Upvotes: 1
Views: 1164
Reputation: 1109
I ran into the same issue just now. My setup is:
export const enum LoadState {
NotStarted,
Loading,
Loaded,
Stale,
}
export type Loadable<T> = {
state: LoadState;
data: T;
};
CachingXProvider
: I use this to request a value for X in some react component, and if it's already fetched and stored in my state, I return it wrapped in Loadable with state = Loaded. Otherwise, I return a Loadable with state = Loading, cache this loading object in my state, and make an API request to get the data (which, on response, updates the cache in the state).I ran into an issue where a react component, in a single render, would request a piece of data twice from the caching data provider. The second request would once again make an API request, because even though the first request dispatched an action to update the redux state to remember the fact that we already requested the item, the store update wouldn't be completed until after the current render was done. This means the cache would miss on the second request, and another API call would get made.
My solution so far is: Have my caching data provider keep its own static list of items that it is requesting from the API for the first time. If any such item is requested again in the lifecycle of the provider, serve it from that list.
Upvotes: 0
Reputation: 194
The second reducer is doing the thing you want because it is directly manipulating the state which is a very bad practice.
Never manipulate a state directly
Now coming to the question, what if you want the data in the same useEffect. Well if you are setting a state inside a useEffect and you want the state back in the same useEffect, you can use the data directly without getting it from the state.
Considering your example, the ideal implementation should be:
const reduxState = useSelector(state=>state)
const [toggle,setToggle] = useState(false)
React.useEffect(() => {
const name = toggle?'Joe':'Bob'
dispatch({type: 'changeName', name}));
console.log('name is:', name);
}, [toggle]);
React.useEffect(() => {
console.log('name in redux is:', reduxState.name);
}, [reduxState.name]);
Although, you said, that you need to use the data in same useEffect, to do this you can use the variable directly. The redux state will anyways get updated and before that you can use the same variable you used in dispatch can be used.
const reduxState = useSelector(state=>state)
const [toggle,setToggle] = useState(false)
React.useEffect(() => {
const name = toggle?'Joe':'Bob'
dispatch({type: 'changeName', name}));
console.log('name is:', name, 'name in redux is:', name)
}, [toggle]);
Upvotes: 2