简体   繁体   中英

React hooks function dependency

I am finding myself in a weird situation. I am implementing an hook and I cannot manage to achieve what I want.

I have something like this:

const appHook = props => {
  const [foo, setFoo] = React.useState([]);
  const [bar, setBar] = React.useState([]);

  React.useEffect(() => {
    setFoo(getFoo(props.fooList, props.fooId));
    setBar(getBar(foo.listToFilter));
  }, [props.fooId]);

  const getCurrentBlockTrade = (arrayToFilter, number) =>
    arrayToFilter.filter(array => array.id === number);

  const getSubOutList = (...) => {
   ...
  };

return (<div>something</div>)
}

My issue is that the function setFoo is properly executed, so foo state is a new array, but setBar that depends on the state of foo, receives an empty array. Basically setBar is executed before setFoo finished so the getBar function receives an empty array.

What is the right way to manage this kind of dependency?

Thanks, F.

TL;DR; Your solution is likely kind user 's answer

Below I'll describe what I've thought and learned so far throughout researches, and come up with 5 suggestions/solutions from people, via blogs,...


You've said:

My issue is that the function setFoo is properly executed, so foo state is a new array, but setBar that depends on the state of foo, receives an empty array. Basically setBar is executed before setFoo finished so the getBar function receives an empty array .

You're true. Basically because in React (both Hooks and class component), setState is asynchronous. What does it mean? It means that setSomething just tells React to re-render the component later . It doesn't magically replace the const something variable in the current running function — that's not possible.

const [foo, setFoo] = useState(0)

function handleClick() {
  setFoo(42) 
  // we declared foo with const, you "obviously" shouldn't expect this 
  // to "somehow" immediately change `foo` to 42

  console.log(foo); 
  // it's 0 in this render, BUT on next render, `foo` will be 42
}

Solution 1.

The easiest technique to you is to store the newly calculated value of foo in a variable, then use that newly calculated value to both setFoo and setBar - a quite popular technique tho.

React.useEffect(() => {
   const newFoo = getFoo(props.fooList, props.fooId);

   setFoo(newFoo);
   setBar(getBar(newFoo.listToFilter));
}, [props.fooId]);

// or: shouldn't use this, only to demonstrate the callback syntax in 
// the new setState Hook (different than the old callback syntax setState):
React.useEffect(() => {
   setFoo(() => {
       const newFoo = getFoo(props.fooList, props.fooId);
       setBar(getBar(newFoo.listToFilter));
       return newFoo;
   })
}, [props.fooId]);

Solution 2.

Another technique can be found here: https://stackoverflow.com/a/54120692/9787887 is using useEffect to setBar with the dependency list whose foo .

React.useEffect(() => {
    setFoo(getFoo(props.fooList, props.fooId));
}, [props.fooId]);

React.useEffect(() => {
    setBar(getBar(foo.listToFilter));
}, [foo]);

Despite the answer get 27 upvotes, I think it's just overcomplicated the situation, and also (as I know) make the component unnecessarily rerender 2 times instead of 1, should be avoided.


Solution 3.

Another solution that might work is to use async/await to make the state changes triggered asynchronously, to lead the changes not be batched (regarding this answer https://stackoverflow.com/a/53048903/9787887 )

React.useEffect(async () => {
   await setFoo(getFoo(props.fooList, props.fooId));
   await setBar(getBar(foo.listToFilter));
}, [props.fooId]); 
// no, actually this will not work!! it'll throw you an (annoyed) error

// the actual working code is:
React.useEffect(() =>
   const setFooAndBar = async () => {
       await setFoo(getFoo(props.fooList, props.fooId));
       await setBar(getBar(foo.listToFilter));
   }
   setFooAndBar();
}, [props.fooId]);

You see, the working code is again another overcomplicated (and bad) solution, (but should be introduced anyway??).


Solution 4.

Another solution that gaearon mentioned is to use useReducer

  • With Hooks you could also useReducer to centralize state update logic and avoid this pitfall.

Another his insight:

  • the recommended solution is to either use one variable instead of two (since one can be calculated from the other one, it seems), or to calculate the next value first and update them both using it together. Or, if you're ready to make the jump, useReducer helps avoid these pitfalls.

But it again seems to be another overcomplex suggestion to this case, doesn't it?


Solution 5.

The last suggestion is a comment of gaearon , tell you to rethink about your state dependence , is the state dependence really needed?

the best solution is simply to not have state that is calculated from another state. If this.state.y is always calculated from this.state.x , remove this.state.y completely, and only track this.state.x . And calculate what you need when rendering instead


Thank you for being patient enough to read to here :)).

setState is an asynchronous function, that's why you are receiving an empty array in setBar function. Basically you can't be sure that the state will be updated before the second setState evaluates.

Why not to simply refer to the props in both cases?

React.useEffect(() => {
   const newFoo = getFoo(props.fooList, props.fooId);

   setFoo(newFoo);
   setBar(getBar(newFoo.listToFilter));
}, [props.fooId]);

Setting a state is an asynchronus process. So setBar(getBar(foo.listToFilter)); calling this foo is the empty array. You can use another useEffect for this

 React.useEffect(() => {
    setFoo(getFoo(props.fooList, props.fooId));
  }, [props.fooId]);



 React.useEffect(() => {
    setBar(getBar(foo.listToFilter));
 }, [foo]);

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM