0

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 !

gordie
  • 1,637
  • 3
  • 21
  • 41
  • If you can show an example of `props.index` (and include the type of `index`: e.g. `string`/`number`/etc.) and also show the implementation of `sanitizeIndex`, you'll get a better answer. – jsejcksn Jan 25 '22 at 16:18
  • Hi @jsejcksn, I've improved the question. – gordie Jan 26 '22 at 08:07

4 Answers4

2

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 for useCallback 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 (e.g. 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>
jsejcksn
  • 27,667
  • 4
  • 38
  • 62
  • Thanks for the nice answer, it seems very good, I tested it and it works. But well, that's incredibly hard to understand and read. The answer by @Someone Special seems quite easier. What do you think ? – gordie Jan 26 '22 at 18:33
  • @gordie Well, you haven't shown your actual component code, so I can't really say. The only code that my answer would add _inside_ your component would be `const [index, setSanitizedIndex] = useSanitizedIndex(sanitizeIndex, props.index);` (which _replaces_ your line that creates the state using `useState`). "Easy to read" is up to you! – jsejcksn Jan 26 '22 at 19:04
  • @gordie In regard to the other answer: as written, it would throw two runtime errors. – jsejcksn Jan 26 '22 at 19:11
  • 1
    thanks a lot for your kind help ! – gordie Jan 26 '22 at 20:31
  • I'm still trying to understand all of this. I'm stuck with my state not updating correctly. Would you mind have a look at https://pastebin.com/rWGQD8iM ? – gordie Jan 28 '22 at 16:02
  • @gordie After a quick look, I don't see how that code relates to the question you've asked here. You are welcome to [ask a new question](https://stackoverflow.com/questions/ask), and if you want to link me to it here, you are welcome to do that. Be sure to include a [minimal, reproducible example](https://stackoverflow.com/help/minimal-reproducible-example) if you post a new question (no pseudocode, undefined variable references, etc.) so that anyone who wants to help you can reproduce the issue. – jsejcksn Jan 28 '22 at 16:08
0

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.

Chris Hitchcock
  • 379
  • 2
  • 13
0

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.

Someone Special
  • 12,479
  • 7
  • 45
  • 76
0

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

syntactic
  • 109
  • 6