Published on

Batching in React

Authors
  • avatar
    Name
    Pyae Phyo Win

Introduction

React.js is highly favored by numerous developers for building dynamic user interfaces due to its Component-Based Architecture, Virtual DOM, Declarative Syntax, and other advantages. However, it can be challenging to manage re-renders, especially with multiple state updates. In this article, you can read about how React.js handles those state updates to improve performance by reducing re-renders.

State in React

State, known as a component's memory, can be used to keep tracks of changes and apply the changes to the interfaces after user interactions like clicking the button, typing into the form and etc.

State can be defined with useState() hook.

export default function Counter() {
  const [count, setCount] = useState(0)
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  )
}

In the above component named Counter, when you click the button, setCount(count + 1) updates the count by 1 and tells the React to re-render the UI.

Understanding Batching

Batching in React occurs when multiple state updates are performed within a single operation or event handler. For example:

import { useState } from 'react'

export function Profile = () => {
  const [name, setName] = useState('')
  const [email, setEmail] = useState('')
  const [city, setCity] = useState('')

  return (
    <div>
      <button
        onClick={() => {
          setName('alice')
          setEmail('alice@qq.com')
          setCity('beijing')
        }}
      >
        set profile
      </button>
    </div>
  )
}

When you click the button, react won't re-render for each setState() instead React will group groups multiple state updates into a single re-render for better performance.

Before React 18, we only batched updates inside React event handlers. Updates inside of promises, setTimeout, native event handlers, or any other event were not batched in React by default:

Forcing React to re-render

Sometimes, you may want React to re-render for each setState() call. For instance, when adding a new comment and scrolling to the latest comments:

import { useState } from 'react'

export function Comments() {
  const [comments, setComments] = useState<string[]>([])

  return (
    <div>
      <button
        onClick={() => {
          setComments([...comments, 'new comment'])
          scrollIntoLastComment()
        }}
      >
        Add comment
      </button>
    </div>
  )
}

In the above component, clicking the button doesn't scroll to the latest comments immediately because React hasn't re-rendered to update the DOM for setComments([...comments, 'new comment']). How can we address this issue?

The flushSync() function allows you to force React to re-render and update the DOM for every state change by wrapping it around the setState callback:

import { useState } from 'react'
import { flushSync } from 'react-dom'

export function Comments() {
  const [comments, setComments] = useState<string[]>([])

  return (
    <div>
      <button
        onClick={() => {
          flushSync(setComments([...comments, 'new comment']))
          scrollIntoLastComment()
        }}
      >
        Add comment
      </button>
    </div>
  )
}

Wrapping the setState callback with flushSync() instructs React to re-render immediately, updating the DOM and allowing scrollIntoLastComment() to work with the updated DOM.

Note: Using flushSync is uncommon and can impact the performance of your app.