2

setState updates state asynchronously. It's my understanding that, when using a class component, you can do something like this to ensure certain code is executed after one setState updates state:

setState({color: red}, callbackThatExecutesAfterStateIsChanged);

I'm using a functional component & hooks. I'm aware, here, useEffect()'s callback will execute everytime after color state changes and on initial execution.

useEffect(callback, [color]);

How can I replicate similar behaviour as the class component example - that is, to execute a chunk of code once after one setState() successfully changes state and not on initial execution?

tonitone120
  • 1,920
  • 3
  • 8
  • 25
  • one after the first render ? – Dolev Dublon Nov 30 '20 at 18:58
  • Similar Question: https://stackoverflow.com/q/56247433/5669120 – Ketan Ramteke Nov 30 '20 at 18:58
  • Does this answer your question? [How to use \`setState\` callback on react hooks](https://stackoverflow.com/questions/56247433/how-to-use-setstate-callback-on-react-hooks) – Ketan Ramteke Nov 30 '20 at 19:00
  • Thanks @KetanRamteke. It doesn't answer my question because, as my question demonstrates, I know how to use `setState`. My question is about replicating the same behaviour as the first code snippet with hooks in a functional component – tonitone120 Nov 30 '20 at 19:25
  • Why would you need that anyway? `useEffect` can solve most of the real-world problems – glinda93 Nov 30 '20 at 19:37
  • @bravemaster I need functionA to receive the latest update of stateA. I don't want functionA to execute on initial execution. It'd be preferable is functionA only executes when stateA updates once. – tonitone120 Nov 30 '20 at 19:46

2 Answers2

1

If you ask me, there is no safe way to do this with hooks. The problem is that you both have to read and set an initialized state in order to ignore the first update:

const takeFirstUpdate = (callback, deps) => {
  const [initialized, setInitialized] = useState(false);
  const [wasTriggered, setWasTriggered] = useState(false);

  useEffect(() => {
    if (!initialized) {
      setInitialized(true);
      return;
    }
    if (wasTriggered) {
      return;
    }
    callback();
    setWasTriggered(true);
  }, [initialized, wasTriggered]);
};

While the hook looks like it works, it will trigger itself again by calling setInitialized(true) in the beginning, thus also triggering the callback.

You could remove the initialized value from the deps array and the hook would work for now - however this would cause an exhaustive-deps linting error. The hook might break in the future as it is not an "official" usage of the hooks api, e.g. with updates on the concurrent rendering feature that the React team is working on.

code-gorilla
  • 2,231
  • 1
  • 6
  • 21
0

The solution below feels hacky. If there's no better alternative, I'm tempted to refactor my component into a class component to make use of the easy way class components allow you to execute code once state has been updated.

Anyway, my current solution is:

The useRef(arg) hook returns an object who's .current property is set to the value of arg. This object persists throughout the React lifecycle. (Docs). With this, we can record how many times the useEffect's callback has executed and use this info to stop code inside the callback from executing on initial execution and for a second time. For example:

initialExecution = useRef(true);
[color, setColor] = useState("red");

useEffect(() => {
  setColor("blue");
});

useEffect(() => {
  if (initialExecution.current) {
    initialExecution.current = false;
    return;
  }
  //code that executes when color is updated.
}, [color]);
tonitone120
  • 1,920
  • 3
  • 8
  • 25