跪拜 Guibai
← Back to the summary

What Actually Runs When a React Component Re-renders

This is the second article in the "React Page Issues Handbook" series.

An order editing page displays the unit price of a product, the purchase quantity, and the order subtotal. After clicking Add One, the page only changes one number, but the log placed inside the component function in the console prints again.

OrderEditor executing
OrderEditor executing

Adding logs to the total price calculation method, event handler, and Effect shows different output counts. What is this problem?

1. Why a State Update Causes the Component Function to Execute Again

The order editing component holds the purchase quantity. What needs to be observed now is the log in the component function and where the state update occurs after clicking the button.

import { useState } from 'react'

export default function OrderEditor() {
  const [quantity, setQuantity] = useState(1)

  console.log('OrderEditor executing, quantity =', quantity)

  function increase() {
    setQuantity(previous => previous + 1)
  }

  return (
    <section>
      <p>Purchase Quantity: {quantity}</p>
      <button onClick={increase}>
        Add One
      </button>
    </section>
  )
}

On the first display, React calls OrderEditor(), and this function execution gets:

quantity = 1

The component returns JSX containing Purchase Quantity: 1, and the page subsequently displays the corresponding content.

After the user clicks Add One, the event handler submits a state update. React will call the component function again using the new state, this time getting:

quantity = 2

Therefore, the console prints again:

OrderEditor executing, quantity = 2

This indicates that React calls the component function again to calculate a new interface description. It does not mean the browser has already modified the DOM, much less that the entire webpage has reloaded.

2. Which Code Recalculates When the Component Function Executes Again

Ordinary code inside the component function body will re-execute with this function call. Below, unit price, total price calculation, and button text are added:

import { useState } from 'react'

export default function OrderEditor() {
  const [quantity, setQuantity] = useState(1)

  const unitPrice = 99
  const totalPrice = unitPrice * quantity
  const buttonText = quantity >= 5
    ? 'Reached single purchase limit'
    : 'Add One'

  function increase() {
    setQuantity(previous => {
      return Math.min(previous + 1, 5)
    })
  }

  return (
    <section>
      <p>Unit Price: {unitPrice} Yuan</p>
      <p>Quantity: {quantity}</p>
      <p>Subtotal: {totalPrice} Yuan</p>

      <button
        disabled={quantity >= 5}
        onClick={increase}
      >
        {buttonText}
      </button>
    </section>
  )
}

Every time quantity updates, the component function executes again, and the following content will all get new results:

unitPrice
totalPrice
buttonText
increase
The returned JSX

unitPrice is still 99 each time, and totalPrice recalculates based on the new quantity.

There is no need to save totalPrice as another piece of State here. It is entirely derived from unitPrice and quantity, calculated directly during rendering, which avoids needing to synchronize the total price additionally after a quantity update.

The event handler increase is also recreated during this component execution. It will use the variables and Props from this round of rendering. The recreation of the function itself does not necessarily mean the page will slow down; it is only worth further optimization if it actually expands the scope of expensive calculations or child component updates.

3. Re-executing the Component Function Does Not Reinitialize All State

After the component function is called again, the code runs to:

const [quantity, setQuantity] = useState(1)

The page, however, will not revert to 1.

The 1 in useState(1) is used for the initial state initialization. During subsequent re-renders, React returns the currently saved state to this component execution.

First execution:

useState(1)
→ quantity = 1

Re-execution after one click:

useState(1)
→ quantity = 2

Therefore, although the component function reruns from the beginning, the State is not overwritten just because it sees the initial value 1.

Ordinary local variables do not have this retention relationship:

export default function OrderEditor() {
  const [quantity, setQuantity] = useState(1)

  let renderCount = 0
  renderCount++

  return (
    <>
      <p>Quantity: {quantity}</p>
      <p>Execution Count: {renderCount}</p>
      <button
        onClick={() => {
          setQuantity(previous => previous + 1)
        }}
      >
        Add One
      </button>
    </>
  )
}

Every component execution recreates renderCount, so it always starts from 0 and then becomes 1. Ordinary local variables are suitable for storing calculation results for the current round but not for storing data that needs to persist across multiple renders.

Data that needs to be displayed on the page and trigger updates is usually placed in State; data that needs to persist across renders but does not require a page update when changed can be considered for Ref. The choice between the two depends on whether the data participates in the interface display.

4. Why the DOM Is Not Completely Rebuilt After Returning New JSX

After the component function executes again, it returns a new piece of JSX:

return (
  <section>
    <p>Unit Price: 99 Yuan</p>
    <p>Quantity: {quantity}</p>
    <p>Subtotal: {totalPrice} Yuan</p>
    <button onClick={increase}>
      Add One
    </button>
  </section>
)

JSX is the description of the interface for this render. React compares this result with the previous result and then commits only the necessary changes to the DOM.

When the quantity changes from 1 to 2:

Page Content Previous Result New Result DOM Change Needed?
Unit Price 99 99 No
Quantity 1 2 Yes
Subtotal 99 198 Yes
Button Text Add One Add One No

Component function execution and DOM modification occur in different phases. Below, the state update, recalculation, and page commit from a single click are placed on the same line.

image.png

In the diagram, it is crucial to distinguish between returning new JSX and modifying the DOM. React completes the component calculation first, then decides which DOM content needs updating based on the before and after results. Nodes for the unchanged unit price and button do not need to be recreated, which is a common performance optimization technique.

The calculations inside the component function re-executed, but the page ultimately only needs to update the text corresponding to the quantity and subtotal.

React officially divides this process into Render and Commit:

So, seeing the component log print again only indicates that the component function was called; it cannot directly prove that all DOM nodes were replaced.

5. Does Effect Follow Every Component Execution

When the component re-executes, the useEffect call inside the component function is also passed through again, but whether the synchronous logic inside the Effect re-executes depends on whether its dependencies have changed.

The following page records which order is currently being edited based on orderId:

import {
  useEffect,
  useState,
} from 'react'

export default function OrderEditor({
  orderId,
}) {
  const [quantity, setQuantity] = useState(1)

  useEffect(() => {
    console.log('Start syncing order:', orderId)

    return () => {
      console.log('Stop syncing order:', orderId)
    }
  }, [orderId])

  return (
    <button
      onClick={() => {
        setQuantity(previous => previous + 1)
      }}
    >
      Quantity: {quantity}
    </button>
  )
}

When the button is clicked, quantity changes, and OrderEditor re-executes. Since orderId hasn't changed, this Effect does not need to resynchronize because of the quantity change.

When the parent passes in a new orderId, React will, during the appropriate commit phase, first handle the cleanup of the old Effect, then execute the Effect with the new orderId.

Here, a distinction must be made:

Component function re-execution
≠ Every Effect re-synchronizes

Effects are used to stay synchronized with external systems outside of React, such as requests, subscriptions, timers, or browser events. Content like total price and button text, which can be derived from current Props and State, should be calculated directly during the render phase, without needing to be stored in State first and then synchronized with an Effect.

When Strict Mode is enabled in the development environment, React may call component functions extra times and perform additional setup and cleanup checks on Effects to expose impure rendering and incomplete cleanup issues. Console logs may therefore be more numerous than expected; the execution count in the production environment cannot be inferred solely from the development environment log count.

6. Why Modifying External Data During Rendering Is Not Suitable

Since the component function may execute multiple times, code during rendering should only be responsible for calculating JSX based on the inputs.

The following code directly sends statistical data inside the component function:

const records = []

export default function OrderEditor({
  orderId,
}) {
  records.push({
    orderId,
    time: Date.now(),
  })

  return <p>Order: {orderId}</p>
}

Every re-render appends a record to the array outside the component. Page state updates, parent component updates, or development environment checks can all increase the number of records. The rendering result is no longer determined solely by the current Props, and the execution count begins to affect external data.

Similar operations also include:

Operations that clearly occur after a user click can be placed in event handlers; operations that need to synchronize with external systems following the component's display state can be placed in Effects, supplemented with dependencies and cleanup logic.

Work that can be safely done during rendering is typically calculations that do not change the external environment:

const totalPrice = unitPrice * quantity
const canSubmit = quantity > 0
const visibleItems = items.filter(item => {
  return item.visible
})

Even if these expressions execute again, as long as the same inputs yield the same results, the external state will not be changed by the number of executions.

7. Does Component Function Re-execution Need Immediate Optimization

Seeing multiple component logs printed on the page does not mean a performance problem already exists.

When the work involved in re-execution is very light, such as string concatenation, conditional checks, and small array processing, adding extra useMemo, useCallback, or memo may only make the code harder to read.

Situations that require further investigation are usually more specific:

You can first use React DevTools Profiler to observe which components participated in the commit and how long an update took, before deciding whether to split state, reduce component scope, or add memoization optimizations.

8. Where to Look First When Investigating Re-renders

When encountering a problem like why did this component execute again, you can check in the following order.

First, confirm who submitted the state update:

setQuantity(previous => previous + 1)

When the state belongs to the current component, it requests the current component to re-render.

Second, check what ordinary calculations are inside the component function body. Local variables, conditional checks, array processing, and function declarations will re-execute with the component call.

Third, distinguish between component execution and DOM commit. Repeated logs do not mean all nodes were modified; you need to observe the page result or use development tools to check the commit scope.

Fourth, check if side effects are misplaced. Requests, subscriptions, and timers should not be written directly in the rendering process.

Fifth, check Effect dependencies. When the component re-executes, whether the Effect re-synchronizes depends on the dependencies and specific configuration; the two cannot be directly equated.

Sixth, confirm whether Strict Mode is enabled in the development environment. Extra logs may come from development checks or from real state updates, requiring judgment based on the triggering operation.

9. Summary

After a state update, React calls the relevant component function again. Local calculations, conditional checks, event functions, and JSX in the function body all get new results based on this round's Props and State. The State itself is preserved by React across multiple renders and will not return to its initial value just because the component executes from the beginning.

After the component returns new JSX, React compares the before and after results and only commits the necessary changes to the DOM. A component log printing twice and page nodes being rebuilt twice are not the same thing. Effects also have their own dependency and cleanup rules; you cannot infer that every side effect re-executes just because the component executed.

Next time you investigate a re-render, first find the source of the update, then check the calculations in the function body, Effect dependencies, and the final DOM changes. Only after confirming that the render scope or calculation cost is causing a real problem should you consider component splitting and memoization optimization.

References

Illustration and Flowchart Suggestions

Figure 1: Rendering and Commit in a State Update

Suggested Insertion Point:

Place after the comparison table in "## 4. Why the DOM Is Not Completely Rebuilt After Returning New JSX".

Transition Sentence Before Figure:

Explanatory Sentence After Figure:

Figure Caption:

Figure 1: After the quantity update, the component recalculates the JSX, and React only commits the changes to the quantity and subtotal.

Chinese Drawing Prompt:

Pre-Publication Checklist

Comments

Top 1 from juejin.cn, machine-translated. The original thread is authoritative.

亚雷

The rendering pipeline is explained in great detail.