skip to Main Content

I have a child component that does a fetch in a useEffect then calls a passed in onChange handler. The child component gets re-rendered 3 times (and does the fetch 3 times) all because the parent’s onChange function "changes".

Requiring the parent to wrap onChange in a useCallback would work but seems wrong as the parent shouldn’t have to care about how the child is implemented. I could also get the child to cache onChange (maybe with useRef or a custom hook like usePrevious) and check in the useEffect if it has changed, but this also seems a little over the top.

Is there a best practice for this sort of pattern? (Ideally without requiring redux or similar)

import { useEffect, useState } from 'react';

const Child = ({ onChange, value }) => {
  useEffect(() => {
    console.log("called");
    fetch(`blah/${value}`)
      .then((result) => onChange(result))
      .catch((error) => onChange(error));
  }, [value, onChange]);

  return <div>Child</div>;
};

export default function Parent () {
  const [result, setResult] = useState();

  return (
    <div>
      {result}
      <Child 
        value={0}
        onChange={r => {
          setResult(r.toString());
        }} 
      />
    </div>
  );
}

codesandbox

2

Answers


  1. If you want to track value of something, you have to memoize its reference somehow. useCallback, useRef, useState, useMemo… Since on each render functions get new reference and that triggers things that tracks those functions.

    Requiring the parent to wrap onChange in a useCallback would work but
    seems wrong as the parent shouldn’t have to care about how the child
    is implemented.

    The parent does not care how the children will use it, it just memoizes the callback for general purpose.

    Login or Signup to reply.
  2. Requiring the parent to wrap onChange in a useCallback would work but
    seems wrong as the parent shouldn’t have to care about how the child
    is implemented.

    This isn’t about a parent component caring about how a child component uses a passed prop or any of its implementation, it’s about providing a stable reference to consumers and not triggering unnecessary rerenders. In other words, it’s a performance enhancement.

    React components rerender for a couple reasons, either their state or props update (e.g. via new references), or the parent component rerenders.

    You are passing an anonymous callback function as the onChange prop, so any time this parent component renders, it’s providing a new onChange function reference and will certainly trigger a child rerender.

    Is there a best practice for this sort of pattern?

    Yes, memoize the callback that is passed down so it’s provided as a stable reference.

    Memoize an onChange handler function in order to provide a stable function reference to the child component so it only rerenders to the DOM when the value prop updates.

    import { useCallback, useEffect, useState } from 'react';
    
    export default function Parent () {
      const [result, setResult] = useState();
    
      const onChangeHandler = useCallback(r => {
        setResult(r.toString());
      }, [setResult]);
    
      return (
        <div>
          {result}
          <Child 
            value={0}
            onChange={onChangeHandler} 
          />
        </div>
      );
    }
    
    const Child = ({ onChange, value }) => {
      useEffect(() => {
        console.log("called");
        fetch(`blah/${value}`)
          .then(onChange)
          .catch(onChange);
      }, [value, onChange]);
    
      return <div>Child</div>;
    };
    

    So now React can tell that the onChange reference doesn’t change render to render, and so long as the value prop doesn’t update, React can bail on child component (and its entire sub-ReactTree) rerenders even though the parent component rerendered. Again, this is just a performance optimization to cut down on unnecessary rerenders.

    See the useCallback hook documentation for further details and other reasons to memoize function references.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search