Frontend Development 12 min read

Why Do Function Components Re‑Render? A Deep Dive into Hooks and Optimization

This article explains the three main reasons function components re‑render—useState/useReducer updates, parent updates, and context changes—and shows how to control unnecessary renders with techniques such as memoization, useCallback, useRef, and component extraction.

Tencent IMWeb Frontend Team
Tencent IMWeb Frontend Team
Tencent IMWeb Frontend Team
Why Do Function Components Re‑Render? A Deep Dive into Hooks and Optimization

Function component re‑render can be triggered by three main situations: updates via

useState

/

useReducer

, parent component updates, and context changes.

1. Updates caused by useState or useReducer

1.1 Regular usage

Example Counter component that logs "counter render" on each click.

<code>const Counter = () => {
  console.log('counter render');
  const [count, addCount] = useState(0);
  return (
    <div className="counter">
      <div className="counter-num">{count}</div>
      <button onClick={() => {addCount(count + 1)}}>add</button>
    </div>
  );
}</code>

1.2 Immutable state

When state is a reference type, React compares old and new state with

Object.is

; if they are equal, no re‑render occurs.

<code>const Counter = () => {
  console.log('counter render');
  const [count, addCount] = useState({ num: 0, time: Date.now() });
  const clickHandler = () => {
    count.num++;
    count.time = Date.now();
    addCount(count);
  };
  return (
    <div className="counter">
      <div className="counter-num">{count.num}, {count.time}</div>
      <button onClick={clickHandler}>add</button>
    </div>
  );
}</code>

Therefore state must be immutable and a new value must be returned for an update to be effective.

1.3 Forced update

Function components lack

forceUpdate

, but you can simulate it by updating a dummy state created with

useState({})

.

<code>const [, forceUpdate] = useState({});
forceUpdate({});</code>

2. Parent component updates

2.1 Regular usage

Adding a child component

Hello

causes it to re‑render on every parent state change, even though its props do not change.

<code>const Hello = ({ name }) => {
  console.log('hello render');
  return <div>hello {name}</div>;
};

const App = () => {
  console.log('app render');
  const [count, addCount] = useState(0);
  return (
    <div className="app">
      <Hello name="react" />
      <div className="counter-num">{count}</div>
      <button onClick={() => {addCount(count + 1)}}>add</button>
    </div>
  );
};</code>

2.2 Optimizing component design

2.2.1 Extract updating part into a separate component

Move the counter logic into its own

Counter

component so

Hello

is not affected.

<code>const App = () => {
  console.log('app render');
  return (
    <div className="app">
      <Hello name="react" />
      <Counter />
    </div>
  );
};</code>

2.2.2 Use children slots for non‑changing parts

<code>const App = ({ children }) => {
  console.log('app render');
  const [count, addCount] = useState(0);
  return (
    <div className="app">
      {children}
      <div className="counter-num">{count}</div>
      <button onClick={() => {addCount(count + 1)}}>add</button>
    </div>
  );
};

// Usage
<App>
  <Hello name="react" />
</App></code>

2.3 React.memo

Class components have

PureComponent

and

shouldComponentUpdate

. Function components can use

React.memo

to skip re‑render when props are shallowly equal.

<code>const Hello = React.memo(({ name }) => {
  console.log('hello render');
  return <div>hello {name}</div>;
});

const App = () => {
  console.log('app render');
  const [count, addCount] = useState(0);
  return (
    <div className="app">
      <Hello name="react" />
      <div className="counter-num">{count}</div>
      <button onClick={() => {addCount(count + 1)}}>add</button>
    </div>
  );
};</code>

Memo uses

shallowEqual

by default; if a prop is a newly created function each render, memo will still re‑render.

2.3.1 useCallback

Wrap callbacks with

useCallback

to keep the same reference, preventing unnecessary re‑renders.

<code>const clickHandler = useCallback(() => {
  console.log('hello click');
}, []);</code>

If the callback uses a state value, the dependency array must include that state; otherwise the callback captures a stale value.

2.3.2 useRef & useEffect

Store mutable values in a ref and update them in

useEffect

; callbacks can read

ref.current

to get the latest state without changing their identity.

<code>const App = ({ children }) => {
  console.log('counter render');
  const [count, addCount] = useState(0);
  const countRef = useRef(count);
  const clickHandler = useCallback(() => {
    console.log('count: ', countRef.current);
  }, [countRef]);
  useEffect(() => {
    countRef.current = count;
  }, [count]);
  return (
    <div className="counter">
      <Hello name="react" onClick={clickHandler} />
      <div className="counter-num">{count}</div>
      <button onClick={() => {addCount(count + 1)}}>add</button>
    </div>
  );
};</code>

Summary of the approach: use

useRef

to hold changing values,

useEffect

to sync them, and

useCallback

to return a stable function.

3. Context updates

Context changes also trigger re‑render; libraries like react‑redux and react‑router rely on this mechanism. For a deeper look, see the referenced article on React Context source analysis.

ReactHooksuseStateuseCallbackReact.memofunction componentsre-render
Tencent IMWeb Frontend Team
Written by

Tencent IMWeb Frontend Team

IMWeb Frontend Community gathering frontend development enthusiasts. Follow us for refined live courses by top experts, cutting‑edge technical posts, and to sharpen your frontend skills.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.