React hooks how to better update multiple states based on props change

10,248

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.

Share:
10,248

Related videos on Youtube

FabioCosta
Author by

FabioCosta

Brazilian fullstack developer with a huge love for: Agile,JS,React, Project management testing and automation.ed.

Updated on June 04, 2022

Comments

  • FabioCosta
    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". enter image description here 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
      Rallen almost 5 years
      I assume your props (initialSortDirection, initialSortParam, initialSortEnabled) do always have a value?
    • FabioCosta
      FabioCosta almost 5 years
      Yes assume they would be properly initialised or have a defaultProps
    • cbdeveloper
      cbdeveloper almost 5 years
      Are you concerned that if you click on a header cell to sort, that sort will overwrite your initial state that came through props?
    • FabioCosta
      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
      cbdeveloper almost 5 years
      Why would you lose the other parameters if you're spreading the other properties: setSortData({ ...currentSortData, hasSort: initialSortEnabled }); . You would only change the hasSort property. By the way, in this case, I would recommend the functional form of setState() to work with state that depends on the previous state: setSortData((prevState) => {return ({...prevState, hasSort: initialSortEnabled});}
    • FabioCosta
      FabioCosta almost 5 years
      You 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
    FabioCosta almost 5 years
    Ok 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
    Haim763 almost 5 years
    Why would they alter the state if you use ...currentSortData?
  • FabioCosta
    FabioCosta almost 5 years
    Let'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
    Haim763 almost 5 years
    So 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
    FabioCosta almost 5 years
    Yeah 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
    cbdeveloper almost 5 years
    Why 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
    FabioCosta almost 5 years
    Because 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
    FabioCosta almost 5 years
    Consider 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
    cbdeveloper almost 5 years
    @FabioCosta I've updated my answer. See if that would work for you.
  • FabioCosta
    FabioCosta almost 5 years
    Exactly! Did not thought about using a ref to keep the old props. Thanks!
  • cbdeveloper
    cbdeveloper almost 5 years
    Glad 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
    FabioCosta almost 5 years
    Yeah 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