Search code examples
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:

  • Location 1: in the render code. This is not allowed based on what I read here:

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

  • Location 4: in an event handler. A bunch of examples (like e.g. this one) in the React docs do this, so this should be allowed.

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

I found nothing so far for:

  • Location 3: on complete of some async operation in the useEffect
  • Location 5: on complete of some async operation in the event handler

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:

    • componentDidMount (quasi-synonymous to useEffect being called on the initial render cycle)

      componentDidMount()

      componentDidMount()
      

      componentDidMount() is invoked immediately after a component is mounted (inserted into the tree). Initialization that requires DOM nodes should go here. If you need to load data from a remote endpoint, this is a good place to instantiate the network request.

      This method is a good place to set up any subscriptions. If you do that, don’t forget to unsubscribe in componentWillUnmount().

      You may call setState() immediately in componentDidMount(). It will trigger an extra rendering, but it will happen before the browser updates the screen. This guarantees that even though the render() will be called twice in this case, the user won’t see the intermediate state. Use this pattern with caution because it often causes performance issues. In most cases, you should be able to assign the initial state in the constructor() instead. It can, however, be necessary for cases like modals and tooltips when you need to measure a DOM node before rendering something that depends on its size or position.

    • componentWillUmount (quasi-synonymous to useEffect hook callback cleanup function)

      componentWillUnmount()

      componentWillUnmount()
      

      componentWillUnmount() is invoked immediately before a component is unmounted and destroyed. Perform any necessary cleanup in this method, such as invalidating timers, canceling network requests, or cleaning up any subscriptions that were created in componentDidMount().

      You should not call setState() in componentWillUnmount() because the component will never be re-rendered. Once a component instance is unmounted, it will never be mounted again.

    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.