FabioCosta
FabioCosta

Reputation: 2749

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

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

Upvotes: 2

Views: 11027

Answers (3)

Kivylius
Kivylius

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

cbdeveloper
cbdeveloper

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

Haim763
Haim763

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

Related Questions