React hooks how to better update multiple states based on props change
Solution 1
Is this the behavior you're looking for?
Here's how I would do it with only one useEffect()
.
I would keep the props
last values (from the previous render) inside an useRef
and would check for differences on each property and decide whether or not I should update the state
. After that, I update the ref
values to the current props
to be checked against the future props
during the next render and so on.
function App() {
const [initialState, setInitialState] = React.useState({
initialProp1: 'A',
initialProp2: 'A'
});
return(
<Table
{...initialState}
setInitialState={setInitialState}
/>
);
}
function Table({initialProp1, initialProp2, setInitialState}) {
const [myState, setMyState] = React.useState({
prop1: initialProp1,
prop2: initialProp2
});
const lastProps = React.useRef({
initialProp1,
initialProp2
});
React.useEffect(()=>{
if (lastProps.current.initialProp1 !== initialProp1) {
console.log('1 changed');
setMyState((prevState)=>{
return({
...prevState,
prop1: initialProp1
});
});
}
if (lastProps.current.initialProp2 !== initialProp2) {
console.log('2 changed');
setMyState((prevState)=>{
return({
...prevState,
prop2: initialProp2
});
});
}
lastProps.current = {
initialProp1,
initialProp2
}
});
function changeState() {
setMyState((prevState) => {
return({
...prevState,
prop2: 'B'
});
});
}
function changeProps() {
setInitialState({
initialProp1: 'A',
initialProp2: 'C'
});
}
return(
<React.Fragment>
<div>This is Table <b>props</b> initialProp1: {initialProp1}</div>
<div>This is Table <b>props</b> initialProp2: {initialProp2}</div>
<div>This is Table <b>state</b> prop1: {myState.prop1}</div>
<div>This is Table <b>state</b> prop2: {myState.prop2}</div>
<button onClick={changeState}>Change Table state</button>
<button onClick={changeProps}>Change props that comes from parent</button>
</React.Fragment>
);
}
ReactDOM.render(<App/>, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>
<div id="root"></div>
Solution 2
You can have one useEffect()
which listens to few states change:
useEffect(() => {
setSortData({
...currentSortData,
sortDirection: initialSortDirection,
sortParam: initialSortParam,
hasSort: initialSortEnabled
});
}, [initialSortDirection, initialSortParam, initialSortEnabled]);
Solution 3
I came across this when I was facing the simliar issue, but was not happy with cbdevelopers answer for his use of useRef
as I was under impression you should not need this. A friendly guy over at reactflux pointed out a more elegant solution:
const Table = (props) => {
const { initialSortDirection, initialSortParam, initialSortEnabled } = props;
const [currentSortData, setSortData] = useState({
sortDirection: initialSortDirection,
sortParam: initialSortParam,
hasSort: initialSortEnabled
});
useEffect(() => {
setSortData((prevSortData) => ({
...prevSortData,
sortDirection: initialSortDirection
}));
}, [initialSortDirection]);
useEffect((prevSortData) => {
setSortData(() => ({
...prevSortData,
sortParam: initialSortParam
});
}, [initialSortParam]);
useEffect(() => {
setSortData((prevSortData) => ({
...prevSortData,
hasSort: initialSortEnabled
}));
}, [initialSortEnabled]);
return (<SomeComponent onChangeSort={setSortData} />)
}
I'm aware you want to merge all into one, but I would not recommend this. You want to separate the concerns, always firing the correct effect when the props update.
https://reactjs.org/docs/hooks-effect.html#tip-use-multiple-effects-to-separate-concerns
Be aware that OP solution is bogus, as if two props update at same time, only the last state change will persists when using multiple effects.
Hope this helps someone.
Related videos on Youtube
FabioCosta
Brazilian fullstack developer with a huge love for: Agile,JS,React, Project management testing and automation.ed.
Updated on June 04, 2022Comments
-
FabioCosta almost 2 years
Say I have a table with sort data and I want to store it on a state (or even 3 separated states). Assume this state could be changed by the child. Is there anyway to do this without having 3 different useEffects, I would like to see if it is possible to achieve the same as below with only 1 use effect?
import React, { useState, useEffect } from "react"; function Table({ initialSortDirection, initialSortParam, initialSortEnabled }) { const [currentSortData, setSortData] = useState({ sortDirection: initialSortDirection, sortParam: initialSortParam, hasSort: initialSortEnabled }); useEffect(() => { setSortData({ ...currentSortData, sortDirection: initialSortDirection }); }, [initialSortDirection]); useEffect(() => { setSortData({ ...currentSortData, sortParam: initialSortParam }); }, [initialSortParam]); useEffect(() => { setSortData({ ...currentSortData, hasSort: initialSortEnabled }); }, [initialSortEnabled]); return (<SomeComponent onChangeSort={setSortData} />) }
On a old school way I would probably use componentWillReceiveProps and just compare nextProps to see if they changed but now I am having difficult on finding a concise way to do it "at once" and only on change.
As a visual example consider the image below, you could change the sort either from clicking on the cell or from changing the "knobs". EDIT 1
Assume that other things could affect the state and I do not want to override an updated state with an unchanged initial prop . I updated the code accordingly
EDIT 2 Added storybook picture
-
Rallen almost 5 yearsI assume your props (
initialSortDirection
,initialSortParam
,initialSortEnabled
) do always have a value? -
FabioCosta almost 5 yearsYes assume they would be properly initialised or have a defaultProps
-
cbdeveloper almost 5 yearsAre you concerned that if you click on a header cell to sort, that sort will overwrite your initial state that came through props?
-
FabioCosta almost 5 years@cbdev420 no, that is the desired behaviour. The concern is that some changed state would be overwritten by an OLD initialState if only one of the initialState changes. A good example would be that if I disable sort by changing the initialSortEnabled I could lose the current sortDirection and sortParam.
-
cbdeveloper almost 5 yearsWhy would you lose the other parameters if you're spreading the other properties:
setSortData({ ...currentSortData, hasSort: initialSortEnabled });
. You would only change thehasSort
property. By the way, in this case, I would recommend the functional form ofsetState()
to work with state that depends on the previous state:setSortData((prevState) => {return ({...prevState, hasSort: initialSortEnabled});}
-
FabioCosta almost 5 yearsYou would lose if you had only ONE useEffect because it would answer for all parameters but you would not know which one changed. I think it boils down to "With only one useEffect is it possible to detect with trigger changed?"
-
-
FabioCosta almost 5 yearsOk my example was not clear, my problem with this one was that if I have other things that alter the state they would be overwritten. Will update my question and upvote your answer
-
Haim763 almost 5 yearsWhy would they alter the state if you use ...currentSortData?
-
FabioCosta almost 5 yearsLet's say that I have a header cell that when clicked needs to be sorted . Then the header cell onClick would call onChangeSort({...someNewParam}) and that the initial values come from another component. I added a picture with this example
-
Haim763 almost 5 yearsSo if you add the params to the list in my example - it will not override the state of other keys. The reason is that when you use the {...} (spread) operator it will take the current states and override just the ones changed.
-
FabioCosta almost 5 yearsYeah this works the problem is that it is still 2 useEffects. My main goal would be to have only one that could update the state only for the changed initialProps
-
cbdeveloper almost 5 yearsWhy would your
initialState
change after the component has rendered? Also, once the initial state has changed, do you want it to overwrite a state that has been set through a column header click, for example? Which one has higher priority? -
FabioCosta almost 5 yearsBecause it can be controlled on outside world. The highest priority is the initialprops if it changes the state should be overwritten and if there is no new initialProps the state would change internally. This was just a example for the question, is there a form to know which dependency changed on useEffect?
-
FabioCosta almost 5 yearsConsider the storybook example. If I change on the knobs below it should update losign the state, if I do nothing the state is what would remain
-
cbdeveloper almost 5 years@FabioCosta I've updated my answer. See if that would work for you.
-
FabioCosta almost 5 yearsExactly! Did not thought about using a ref to keep the old props. Thanks!
-
cbdeveloper almost 5 yearsGlad I could help. I'm brazilian too, living in Portugal. Studying to become a web developer and launch some projects (websites, webapps) of my own. Happy coding to you!
-
FabioCosta almost 5 yearsYeah the problem was that I didn't make it clear was the unchanged initial$SOMETHING that would overwrite if only one of the initial$SOMETHING changed