Reputation: 2749
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
Upvotes: 2
Views: 11027
Reputation: 6547
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.
Upvotes: 1
Reputation: 31335
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>
Upvotes: 3
Reputation: 1654
You can have one useEffect()
which listens to few states change:
useEffect(() => {
setSortData({
...currentSortData,
sortDirection: initialSortDirection,
sortParam: initialSortParam,
hasSort: initialSortEnabled
});
}, [initialSortDirection, initialSortParam, initialSortEnabled]);
Upvotes: 1