javascriptreactjsreact-statereact-lifecycle-hooks

In which locations are you allowed to modify lifted-up state?


It is common in React to lift-up state and passing that state from parent to child, optionally together with a callback function allowing the child to signal to the parent that the state should change.

I can't seem to find in the official React docs where the child component is allowed to call that callback function.

In the following pseudo-code, I've marked 5 different locations where the child could call the callback. I'm trying to find some (official) docs or references on which of those locations are allowed.

function Parent({}){
  const [isActive, setIsActive] = useState(false);

  return (
    <>
      <Foo/>
      <Child active={isActive} onChange={setIsActive}/>
    </>
  );
}

function Child({active,onChange}){
  //Location 1: in the render code
  onChange(old => !old);

  useEffect(() => {
    //Location 2: in a useEffect
    onChange(old => !old);

    //Location 3: on complete of some async operation in the useEffect
    asyncOperationLikeFetch().then( () => onChange(old => !old));
  }, [])

  return (
    <p>{active}</p>
    <button onClick={() => {
      //Location 4: in an event handler
      onChange(old => !old)

      //Location 5: on complete of some async operation in the event handler
      asyncOperationLikeFetch().then( () => onChange(old => !old));
    }}>Toggle</>
  )
}

I found official documentation for:

Also, you can only update the state of the currently rendering component like this. Calling the set function of another component during rendering is an error

I also found some none official docs for (but I would appreciate a pointer to the official docs):

I found nothing so far for:

Anybody can tell me in which locations I can (not) call that callback received from the parent?


Solution

  • You appear to have a correct understanding.

    Case (3) is the same as case (2), enqueueing a state update in useEffect hook callback, and similarly case (5) is the same as case (4) (and cases (2) and (3) really!) enqueueing a state update in a callback function.

    Basically you just want to avoid unintentional side-effects like case (1) where the function is unconditionally called while the function component body is called by React. React components are to be considered Pure Functions.

    Cases (2), (3), (4), and (5) are all considered intentional side-effects, either by being called in the useEffect hook callback, or in some asynchronously called DOM event handler, like a button click handler.

    You could do a bit of a deep dive into the legacy React docs to gain insight and an appreciation of the React Component Lifecycle. There you will find documentation on the older Class-based components' lifecycles and some details on when React state updates can be applied.

    Take for example:

    Note also that you could always enqueue state updates from other DOM event callbacks.

    A diagram I found helpful when learning React was the lifecycle diagram:

    enter image description here

    React function component bodies are effectively the render "method", and is/can be called by React any number of times during the "render phase" where the code/logic is to be "pure and have no side-effects". Here you can't enqueue state updates as these are side-effects. The useEffect hook is synonymous to "componentDidMount", "componentDidUpdate", and "componentWillUnmount" and all the DOM event handlers are called during or after the view has been rendered and committed to the DOM, e.g. the "commit phase", and here you can issue side-effects and schedule updates to the React state.