React.useCallback() is still re-rendering child component

Asked
Active3 hr before
Viewed126 times

9 Answers

renderingreactchildcomponent
90%

Join Stack Overflow to learn, share knowledge, and build your career.,Functional component test-case , Stack Overflow for Teams Where developers & technologists share private knowledge with coworkers

const Block = React.memo(props => {
  console.log("Rendering block: ", props.color);

  return (
    <div
      onClick={props.onBlockClick}
      style={{
        width: "200px",
        height: "100px",
        marginTop: "12px",
        backgroundColor: props.color,
        textAlign: "center"
      }}
    >
      {props.text}
    </div>
  );
});

const Example = () => {
  const [count, setCount] = React.useState(0);
  console.log("Rendering Example. Count: ", count);

  const onClick = () => {
    console.log("I've been clicked when count was: ", count);
  };
  const onClickRef = React.useRef(onClick);
  React.useEffect(
    () => {
      // By leaving off the dependency array parameter, it means that
      // this effect will execute after every committed render, so
      // onClickRef.current will stay up-to-date.
      onClickRef.current = onClick;
    }
  );

  const onClickMemoized = React.useCallback(() => {
    onClickRef.current();
  }, []);

  const updateCount = React.useCallback(() => {
    setCount(prevCount => prevCount + 1);
  }, []);

  return (
    <div style={{ display: "flex", flexDirection: "row" }}>
      <Block
        onBlockClick={onClickMemoized}
        text={"Click me to log with empty array as input"}
        color={"orange"}
      />
      <Block
        onBlockClick={updateCount}
        text={"Click me to add to the count"}
        color={"red"}
      />
    </div>
  );
};

ReactDOM.render(<Example />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js"></script>

<div id='root' style='width: 100%; height: 100%'>
</div>
load more v
88%

“Every callback function should be memoized to prevent useless re-rendering of child components that use the callback function” is the reasoning of his teammates.,These considerations apply to useCallback() hook too. Its appropriate use case is to memoize the callback functions that are supplied to memoized heavy child components.,The list could be big, maybe hundreds of items. To prevent useless list re-renderings, you wrap it into React.memo().

import React, { useCallback } from 'react';

function MyComponent() {
  const handleClick = useCallback(() => {    // handle the click event  }, []);
  return <MyChild onClick={handleClick} />;
}
load more v
72%

Now, the requirements have progressed and we have to house a Reset button for count inside the Child component.,Now, we have to prevent the Child component from rerendering. Keeping the functional component we can use React.memo to achieve this. The child component will become:,even though the child component has nothing to do with the state at all.

Note the console logs as you click re-render. Both child and parent will re-render with logs:

re - render parent componentre - render child component.
re - render parent componentre - render child component.

Now, we have to prevent the Child component from rerendering. Keeping the functional component we can use React.memo to achieve this. The child component will become:

import React, {
   memo
} from 'react';
const Child = memo(({
         reset
      }) => { // same content as earlier});
import React, {
   memo
} from 'react';
const Child = memo(({
         reset
      }) => { // same content as earlier});

Without the second argument, memo will do a shallow comparison of props:

if (prevProps !== props) {
   rerender();
} else { // don't}
if (prevProps !== props) {
   rerender();
} else { // don't}

You can check the logs now and see that it does not update the child component on parent rerender. It only updates the parent component with the log:

re - render parent component
re - render parent component

This would refractor child to:

1import React, { memo } from 'react';23const Child = memo(({ reset }) => {4  console.log('re-render child component.');5  return (6    <div>7      <p>child component which resets count</p>8      <button onClick={reset}>Reset Count</button>9    </div>10  );11});1213export default Child;
1import React, { memo } from 'react';23const Child = memo(({ reset }) => {4  console.log('re-render child component.');5  return (6    <div>7      <p>child component which resets count</p>8      <button onClick={reset}>Reset Count</button>9    </div>10  );11});1213export default Child;

For the reset function we have to refractor the parent to:

1const Parent () => {2  const [count, setCount] = useState(0);3  console.log("re-render parent component");45  const resetCount = () => {6    setCount(0);7  };8  return (9    <main>10      <p>Count: {count}</p>11      <button onClick={() => setCount(count=>(count+1))}>Increment</button>12      <Child reset={resetCount} />13    </main>14  )15}
1const Parent () => {2  const [count, setCount] = useState(0);3  console.log("re-render parent component");45  const resetCount = () => {6    setCount(0);7  };8  return (9    <main>10      <p>Count: {count}</p>11      <button onClick={() => setCount(count=>(count+1))}>Increment</button>12      <Child reset={resetCount} />13    </main>14  )15}

Now, to apply the memo magic again, we need to make sure that resetCount function is not unnecessarily recreated on every render of Parent. This is exactly what useCallback helps us to do.

const resetCount = useCallback(() => {
   setCount(0);
}, [setCount]);
const resetCount = useCallback(() => {
   setCount(0);
}, [setCount]);
load more v
65%

React Hooks useCallback causes child to re-render ,Functional component test-case ,Copyright © 2021 SemicolonWorld. All Rights Reserved.

class Block extends React.PureComponent {
  render() {
    console.log("Rendering block: ", this.props.color);

    return (
        <div onClick={this.props.onBlockClick}
          style = {
            {
              width: '200px',
              height: '100px',
              marginTop: '12px',
              backgroundColor: this.props.color,
              textAlign: 'center'
            }
          }>
          {this.props.text}
         </div>
    );
  }
};

class Example extends React.Component {
  state = {
    count: 0
  }
  
  
  onClick = () => {
    console.log("I've been clicked when count was: ", this.state.count);
  }
  
  updateCount = () => {
    this.setState({ count: this.state.count + 1});
  };
  
  render() {
    console.log("Rendering Example. Count: ", this.state.count);
    
    return (
      <div style={{ display: 'flex', 'flexDirection': 'row'}}>
        <Block onBlockClick={this.onClick} text={'Click me to log the count!'} color={'orange'}/>
        <Block onBlockClick={this.updateCount} text={'Click me to add to the count'} color={'red'}/>
      </div>
    );
  }
};

ReactDOM.render(<Example/>, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>

<div id='root' style='width: 100%; height: 100%'>
</div>
load more v
75%

Now with this setup, every theme change re-renders the App component and all of its children irrespective of whether they consumed the theme or not. Unlike the last example, TickerComponent(2) is also re-rendered in this case.,4th group: Now the theme value is changed in the parent, both the parent (App) component and its children (ticker components) were re-rendered.,When the App component re-renders, its children would re-render irrespective of whether they consume the theme value (as a prop) are not. Checkout this updated example in which the second ticker component doesn’t consume theme state but gets re-rendered when the theme is changed. This is React’s default behavior and it can be altered by wrapping the child components with the useMemo hook which we’ll look into it shortly.

Let’s understand the workings of useState with an example. We will implement the Ticker component as per the above sketch. The active ticker which is shown on the UI is stored in a local state using useState hook. Here is the relevant portion of the component (full code here).

const [ticker, setTicker] = useState("AAPL");...
const onChange = event => {
   setTicker(event.target.value);
}...
load more v
40%

Additional Hooks useReducer useCallback useMemo useRef useImperativeHandle useLayoutEffect useDebugValue ,The setState function is used to update the state. It accepts a new state value and enqueues a re-render of the component.,If no array is provided, a new value will be computed on every render.

const [state, setState] = useState(initialState);
load more v
22%

Running the code we can notice that the first two components are rendered at the same time than the parent, but the third component (where all props are memoized) is rendered only once.,We can notice that each time React renders the parent component, it renders the first component but doesn't render the second one. This is a good example of optimizing rendering of children components., https://felixgerschau.com/react-rerender-components/ for a very detailed post regarding when React renders components.

function CountComponent(props) {
  const countRef = useRef(0);
  useEffect(() => {
    countRef.current = countRef.current + 1;
  });
  return (<div className="counter">
            <p>Current count: {countRef.current} </p>
          </div>);
}
load more v
60%

But if we look at the console, we can see that the Button component is still re-rendering.,Then, we would create two functions: one to increment the counter and other to decrement the counter.,After that, we would create two buttons that would trigger each function, and increment or decrement our counter depending on the button.

Pretag
 Pretag team - issue, fix, solve, resolve
48%

Typing into the input field for adding an item to the list should only trigger a re-render for the App component, but not for its child components which don't care about this state change. Thus, React memo will be used to prevent the child components from updating:,React's useCallback Hook can be used to optimize the rendering behavior of your React function components. We will go through an example component to illustrate the problem first, and then solve it with React's useCallback Hook.,Note: Don't mistake React's useCallback Hook with React's memo API. While useCallback is used to memoize functions, React memo is used to wrap React components to prevent re-renderings.

Let's take the following example of a React application which and allows us to and items with . We are using to make the list stateful:

import React from 'react';import { v4 as uuidv4 } from 'uuid'; const App = () => {  const [users, setUsers] = React.useState([    { id: 'a', name: 'Robin' },    { id: 'b', name: 'Dennis' },  ]);   const [text, setText] = React.useState('');   const handleText = (event) => {    setText(event.target.value);  };   const handleAddUser = ()  =>{    setUsers(users.concat({ id: uuidv4(), name: text }));  };   const handleRemove = (id) => {    setUsers(users.filter((user) => user.id !== id));  };   return (    <div>      <input type="text" value={text} onChange={handleText} />      <button type="button" onClick={handleAddUser}>        Add User      </button>       <List list={users} onRemove={handleRemove} />    </div>  );}; const List = ({ list, onRemove }) => {  return (    <ul>      {list.map((item) => (        <ListItem key={item.id} item={item} onRemove={onRemove} />      ))}    </ul>  );}; const ListItem = ({ item, onRemove }) => {  return (    <li>      {item.name}      <button type="button" onClick={() => onRemove(item.id)}>        Remove      </button>    </li>  );}; export default App;
import React from 'react';import { v4 as uuidv4 } from 'uuid'; const App = () => {  const [users, setUsers] = React.useState([    { id: 'a', name: 'Robin' },    { id: 'b', name: 'Dennis' },  ]);   const [text, setText] = React.useState('');   const handleText = (event) => {    setText(event.target.value);  };   const handleAddUser = ()  =>{    setUsers(users.concat({ id: uuidv4(), name: text }));  };   const handleRemove = (id) => {    setUsers(users.filter((user) => user.id !== id));  };   return (    <div>      <input type="text" value={text} onChange={handleText} />      <button type="button" onClick={handleAddUser}>        Add User      </button>       <List list={users} onRemove={handleRemove} />    </div>  );}; const List = ({ list, onRemove }) => {  return (    <ul>      {list.map((item) => (        <ListItem key={item.id} item={item} onRemove={onRemove} />      ))}    </ul>  );}; const ListItem = ({ item, onRemove }) => {  return (    <li>      {item.name}      <button type="button" onClick={() => onRemove(item.id)}>        Remove      </button>    </li>  );}; export default App;

Using what we learned about (if you don't know React memo, read the guide first and then come back), which has similar components to our example, we want to prevent every component from re-rendering when a user types into the input field.

const App = () => {  console.log('Render: App');   ...}; const List = ({ list, onRemove }) => {  console.log('Render: List');  return (    <ul>      {list.map((item) => (        <ListItem key={item.id} item={item} onRemove={onRemove} />      ))}    </ul>  );}; const ListItem = ({ item, onRemove }) => {  console.log('Render: ListItem');  return (    <li>      {item.name}      <button type="button" onClick={() => onRemove(item.id)}>        Remove      </button>    </li>  );};
const App = () => {  console.log('Render: App');   ...}; const List = ({ list, onRemove }) => {  console.log('Render: List');  return (    <ul>      {list.map((item) => (        <ListItem key={item.id} item={item} onRemove={onRemove} />      ))}    </ul>  );}; const ListItem = ({ item, onRemove }) => {  console.log('Render: ListItem');  return (    <li>      {item.name}      <button type="button" onClick={() => onRemove(item.id)}>        Remove      </button>    </li>  );};

Typing into the input field for adding an item to the list should only trigger a re-render for the App component, but not for its child components which don't care about this state change. Thus, React memo will be used to prevent the child components from updating:

const List = React.memo(({ list, onRemove }) => {  console.log('Render: List');  return (    <ul>      {list.map((item) => (        <ListItem key={item.id} item={item} onRemove={onRemove} />      ))}    </ul>  );}); const ListItem = React.memo(({ item, onRemove }) => {  console.log('Render: ListItem');  return (    <li>      {item.name}      <button type="button" onClick={() => onRemove(item.id)}>        Remove      </button>    </li>  );});
const List = React.memo(({ list, onRemove }) => {  console.log('Render: List');  return (    <ul>      {list.map((item) => (        <ListItem key={item.id} item={item} onRemove={onRemove} />      ))}    </ul>  );}); const ListItem = React.memo(({ item, onRemove }) => {  console.log('Render: ListItem');  return (    <li>      {item.name}      <button type="button" onClick={() => onRemove(item.id)}>        Remove      </button>    </li>  );});

However, perhaps to your surprise, both function components still re-render when typing into the input field. For every character that's typed into the input field, you should still see the same output as before:

// after typing one character into the input field Render: AppRender: ListRender: ListItemRender: ListItem
// after typing one character into the input field Render: AppRender: ListRender: ListItemRender: ListItem

Let's have a look at the that are passed to the List component.

const App = () => {  // How we're rendering the List in the App component   return (    //...    <List list={users} onRemove={handleRemove} />  )}
const App = () => {  // How we're rendering the List in the App component   return (    //...    <List list={users} onRemove={handleRemove} />  )}

Finally we have our use case for React's useCallback Hook. We can use useCallback to memoize a function, which means that this function only gets re-defined if any of its dependencies in the dependency array change:

const App = () => {
      ... // Notice the dependency array passed as a second argument in useCallback  const handleRemove = React.useCallback(    (id) => setUsers(users.filter((user) => user.id !== id)),    [users]  );   ...};
const App = () => {
      ... // Notice the dependency array passed as a second argument in useCallback  const handleRemove = React.useCallback(    (id) => setUsers(users.filter((user) => user.id !== id)),    [users]  );   ...};
load more v

Other "rendering-react" queries related to "React.useCallback() is still re-rendering child component"