2

i have and useEffect inside my component

  useEffect(() => {
    const interval = setInterval(() => {
      setQueueState((pre) => {
        return pre.map((line) => {
          if (line.length === 0) return [];
          line[0].items = line[0].items > 0 ? line[0].items - 1 : 0;
          if (line[0].items > 0) {
            return [
              line[0],
              ...line.slice(1).filter((person) => person.items > 0)
            ];
          } else {
            return [...line.slice(1).filter((person) => person.items > 0)];
          }
        });
      });
    }, 4000);

    return () => {
      clearInterval(interval);
    };
  }, []);

What it basically does is to loop through an array of checkout lines and reduce the number of items of the first person in each line by 1. Now the problem with Strict Mode is that it gets reduced 2 times. I know it's because Strict Mode renders a component 2 times. I just want to know a way to avoid this and still use Strict Mode.

Giorgi Moniava
  • 27,046
  • 9
  • 53
  • 90
ludinj
  • 107
  • 7
  • 1
    unrelated: this is not immutably updating state – Giorgi Moniava Aug 20 '23 at 19:53
  • lmao thank you i rewrote the hook so it does not mutate the state and now it works fine – ludinj Aug 20 '23 at 20:01
  • But it's still valid question in general, see how here the result is 2: https://stackblitz.com/edit/stackblitz-starters-tio6kx?description=A%20create-react-app%20project%20based%20on%20react%20and%20react-dom&file=src%2FApp.js&title=React%20Starter – Giorgi Moniava Aug 20 '23 at 20:04
  • @GiorgiMoniava: I don't think your comment is unrelated. Mutating state is likely the cause of the issue. – Wing Aug 20 '23 at 20:11
  • @Wing See my above link to stackblitz – Giorgi Moniava Aug 20 '23 at 20:12
  • @GiorgiMoniava: the cleanup in this case would be aborting the state change by queuing the reverse of the state change (`() => setA(ps => ps - 1)`). – Wing Aug 20 '23 at 23:32
  • @GiorgiMoniava: cleanup and purity is especially important given concurrency features introduced in React 18 and beyond. – Wing Aug 20 '23 at 23:46
  • @Wing The cleanup function is often used to stop synchronizing the effect (e.g. stop listening to some event), I haven't seen it used to undo the actions performed in the effect as you suggest. – Giorgi Moniava Aug 21 '23 at 07:54
  • @Wing ps. "reversing the operation defined in the effect." what do you mean here? when would you do that? See also my comment above. – Giorgi Moniava Aug 21 '23 at 08:54
  • @GiorgiMoniava: this thread is getting long so I've asked and answered a question, see [How to fix React calling `setState` twice in `useEffect` that has an empty dependency array in strict mode?](https://stackoverflow.com/questions/76945781/how-to-fix-react-calling-setstate-twice-in-useeffect-that-has-an-empty-depen/76945782). This should answer both questions in your comments. Regarding "reversing the operation": in your example you incremented 1 so the reverse is to decrement; this is done in the cleanup function. Please comment on the other question as it's off-topic for this one. – Wing Aug 21 '23 at 13:38

2 Answers2

0

As you pointed out, there are 2 renders with Strict mode on development. On my project, I create a hook called useEffectOnce.

import { useEffect, useRef } from 'react'

export const useEffectOnce = (effect: React.EffectCallback) => {
  const mounted = useRef(false)
  useEffect(() => {
    if (mounted.current) {
      return
    }
    mounted.current = true
    return effect()
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [])
}

and I use it like this:

useEffectOnce(() => {
  // ...
})
Kevin Gilbert
  • 903
  • 8
  • 21
  • Why would you work around the protections offered by strict mode? If you do this you might as well disable strict mode in the first place. The best solution, however, is to ensure components are pure and effects are cleaned up. – Wing Aug 20 '23 at 20:06
  • 1
    @Wing Yeah but here: https://stackblitz.com/edit/stackblitz-starters-tio6kx?description=A%20create-react-app%20project%20based%20on%20react%20and%20react-dom&file=src%2FApp.js&title=React%20Starter, there is no cleanup, component is also pure but result is 2. Just showing that there are sometimes cases when you might get undesired effect maybe ...? – Giorgi Moniava Aug 20 '23 at 20:10
  • @GiorgiMoniava: That's not OP's situation, though. While the answer proposed here may work and may be suitable for certain situations, it is not good practice for OP's situation. This answer would've hidden the impurity of OP's original code and increased the risk of bugs. However, I think in general, I still think this is a bad idea. Ultimately, I think it comes down to a mindset shift to thinking about how React works. – Wing Aug 20 '23 at 23:33
  • @GiorgiMoniava: From React's [documentation](https://react.dev/learn/synchronizing-with-effects#how-to-handle-the-effect-firing-twice-in-development): "React intentionally remounts your components in development to find bugs [...]. **The right question isn’t “how to run an Effect once”, but “how to fix my Effect so that it works after remounting.** [...] Usually, the answer is to implement the cleanup function. The cleanup function should stop or undo whatever the Effect was doing." – Wing Aug 20 '23 at 23:35
  • @GiorgiMoniava: I've responded to your StackBlitz problem in the thread underneath the question. Kevin: you may be interested too given the conversation in this thread on your answer. – Wing Aug 20 '23 at 23:38
  • @Wing No one here is saying we should hack our way around React. The question specifically asked how to avoid having 2 renders in development mode without disabling Strict mode. – Kevin Gilbert Aug 21 '23 at 11:54
  • @KevinGilbert: the solution you're proposing here is the hack. See the quote from React's documentation in my previous comment. The root cause was due to impure state mutation. As mentioned in a previous comment, this solution would've hidden that problem instead of actually solving it. Regarding the more generic problem proposed in the StackBlitz, I've elaborated more on it in this [Q&A](https://stackoverflow.com/questions/76945781/how-to-fix-react-calling-setstate-twice-in-useeffect-that-has-an-empty-depen/76945782). – Wing Aug 21 '23 at 13:45
0

thanks to the coment from Giorgi Moniava i rewrote the hook so it does not mutate the state and now it works as i wanted this is how the hook looks now

 useEffect(() => {
    const interval = setInterval(() => {
      setQueueState((prevState) => {
        const newQueueState = prevState.map((line) => {
          if (line.length === 0) return [];
          const firstPerson = {
            ...line[0],
            items: Math.max(0, line[0].items - 1)
          };
          const filteredLine = line
            .slice(1)
            .filter((person) => person.items > 0);

          if (firstPerson.items > 0) {
            return [firstPerson, ...filteredLine];
          } else {
            return filteredLine;
          }
        });

        return newQueueState;
      });
    }, 4000);

    return () => {
      clearInterval(interval);
    };
  }, []);
ludinj
  • 107
  • 7