简体   繁体   中英

React hooks : how can I update a state within useEffect when the state itself is the dependency?

I know there already are already some related questions, like How can React useEffect watch and update state? , but still, I don't get it totally.

Let's say I set an index state based on a prop; and I need to sanitize that value anytime it is set.

<MyComponent index={4}/>

This is how I attempted to do it:

useEffect(() => {
  setIndex(props.index);
}, [props.index]);

useEffect(() => {
  const sanitized = sanitizeIndex(index);
  setIndex(sanitized);
},[index])

const sanitizeIndex = index => {
    //check that index exists in array...
    //use fallback if not...
    //etc.
    return index
}

It does not work (infinite loop), since the state is watched and updated by the second useEffect() .

Of course, I could avoid this by calling sanitizeIndex() on the prop, so I only need a single instance of useEffect() :

useEffect(() => {
  setIndex(sanitizeIndex(props.index));
}, [props.index]);

Thing is, I call setIndex plenty of times in my code, and I fear to miss using sanitizeIndex .

Is there another way to "catch" and update a state value being set?

Thanks !

So think of useEffect like an event listener in javascript. It's not the same thing, but think of it like that. The event or "what's being watched", in this case, you've asked it to watch props.index. It will run that function inside the useEffect every time anything in the dependency array (props.index - in your case) changes. So what's happening here is you're updating props.index every time props.index changes. This is your infinite loop.

Couple things here, create a copy of props.index as something ie.
const [something, setSomething = useState(props.index);
(I won't get into destructuring, but worth looking that up too) You don't want to manipulate your props directly the way you are doing.

This solves that, as well as gives you the correct way to look at your useEffect. Since you want to update something whenever that prop changes, you could leave props.index (again look up destructuring) in your dependency array, and change the useEffect to:

const [something, setSomething] = useState(props.index);

useEffect(() => {
  setSomething(props.index);
}, [props.index]);

As another pointed out, this is difficult without knowing exactly what you're doing, but this is kind of an overview which hopefully helps you understand what is going on here and why you're getting a loop here.

You mentioned you fear missing out on sanitizing, then you should not be using setIndex directly. Instead, you can create a new function to santize and set the index.

useEffect(() => {
  setSanitizeIndex(props.index);
}, [props.index]);

const setSanitizeIndex = (value) => {
    const sanitizeIndex = sanitizeIndex(value);
    setIndex(sanitizeIndex)
}

With that, you should not be calling setIndex any more in your codes, but only call setSanitizeIndex .

This seems like a good case for a custom hook . Here's an example of how to implement one for your case (given the information currently provided in your question), including comments about how/why:

Be sure to read the documentation foruseCallback if you are not already familiar with it. It's especially important to understand how to use the dependency array ( link 1 , link 2 ) when using hooks which utilize it (like useCallback and useEffect ).

 <div id="root"></div><script src="https://unpkg.com/react@17.0.2/umd/react.development.js"></script><script src="https://unpkg.com/react-dom@17.0.2/umd/react-dom.development.js"></script><script src="https://unpkg.com/@babel/standalone@7.16.12/babel.min.js"></script> <script type="text/babel" data-type="module" data-presets="env,react"> const {useCallback, useEffect, useState} = React; /** * You didn't show exactly how you are sanitizing, so I'm using this function * instead. It will simply make sure the input number is always even by * adding 1 to it if it's odd. */ function makeEven (n) { return n % 2 === 0? n: n + 1; } function useSanitizedIndex (sanitizeIndex, unsanitizedIndex) { const [index, setIndex] = useState(sanitizeIndex(unsanitizedIndex)); // Like setIndex, but also sanitizes const setSanitizedIndex = useCallback( (unsanitizedIndex) => setIndex(sanitizeIndex(unsanitizedIndex)), [sanitizeIndex, setIndex], ); // Update state if arguments change useEffect( () => setSanitizedIndex(unsanitizedIndex), [setSanitizedIndex, unsanitizedIndex], ); return [index, setSanitizedIndex]; } function IndexComponent (props) { // useCallback memoizes the function so that it's not recreated on every // render. This also prevents the custom hook from looping infinintely const sanitizeIndex = useCallback((unsanitizedIndex) => { // Whatever you actually do to sanitize the index goes in here, // but I'll just use the makeEven function for this example return makeEven(unsanitizedIndex); // If you use other variables in this function which are defined in this // component (eg you mentioned an array state of some kind), you'll need // to include them in the dependency array below: }, []); // Now simply use the sanitized index where you need it, // and the setter will sanitize for you when setting it (like in the // click handler in the button below) const [index, setSanitizedIndex] = useSanitizedIndex(sanitizeIndex, props.index); return ( <div> <div>Sanitized index (will always be even): {index}</div> <button onClick={() => setSanitizedIndex(5)}>Set to 5</button> </div> ); } function Example () { const [count, setCount] = useState(0); return ( <div> <div>Count: {count}</div> <button onClick={() => setCount(n => n + 1)}>Increment</button> <IndexComponent index={count} /> </div> ); } ReactDOM.render(<Example />, document.getElementById('root')); </script>

One potential solution for this that I have done in the past with a similar issue is to indirectly trigger the useEffect. Create a dummy state that does not relate to the state being updated and update the dummy state whenever you want the effect to be triggered.

const [dummyState, setDummyState] = useState(0)
useEffect(() => {
  setIndex(props.index);
}, [props.index]);

useEffect(() => {
  const sanitized = sanitizeIndex(index);
  setIndex(sanitized);
},[dummyState])

const sanitizeIndex = index => {
    //check that index exists in array...
    //use fallback if not...
    //etc.
    return index
}
return (
    //update the state you want to update then update the dummy state to 
    //trigger the useEffect
    <button onClick={() =>{setIndex(123);
                           setDummyState(dummyState++);}/>
)

I think the accepted answers solution is a lot less janky but in more complex situations this might be your easiest and most easy-to-understand solution

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