2

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

6
  • I assume your props (initialSortDirection, initialSortParam, initialSortEnabled) do always have a value? Commented Jun 6, 2019 at 8:07
  • Yes assume they would be properly initialised or have a defaultProps Commented Jun 6, 2019 at 8:29
  • Are you concerned that if you click on a header cell to sort, that sort will overwrite your initial state that came through props? Commented Jun 6, 2019 at 9:24
  • @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. Commented Jun 6, 2019 at 9:27
  • 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});} Commented Jun 6, 2019 at 9:39

3 Answers 3

3

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>

Sign up to request clarification or add additional context in comments.

7 Comments

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
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?
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?
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
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!
|
1

You can have one useEffect() which listens to few states change:

useEffect(() => {
  setSortData({
    ...currentSortData,
    sortDirection: initialSortDirection,
    sortParam: initialSortParam,
    hasSort: initialSortEnabled
  });
}, [initialSortDirection, initialSortParam, initialSortEnabled]);

5 Comments

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
Why would they alter the state if you use ...currentSortData?
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
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.
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
1

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.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.