Understanding flushSync: Mastering Batching Behavior in ReactJS

Understanding flushSync: Mastering Batching Behavior in ReactJS

Overview of React's Rendering Behavior

React's rendering behavior is designed to be efficient and fast. When state changes occur, React updates the component tree and re-renders the UI.

However, not every state change leads to an immediate re-render. This is where batching comes into play.

Before Batching Behavior

In early versions (React 17 and earlier), React updated the DOM immediately after each state change.

While straightforward, this approach could lead to performance issues in complex applications. Multiple state updates within a single event cycle would cause multiple, unnecessary re-renders, affecting the application's responsiveness.

function WithoutBatchingExample() {
  const [count, setCount] = useState(0);
  const [flag, setFlag] = useState(false);

  const handleUpdate = () => {
    // Pretend React does not batch these: each would cause a re-render
    setCount(count + 1); // First update
    setFlag(!flag);      // Second update
    // In pre-batching React, this would cause two separate renders
  };

  console.log('Component renders');

  return (
    <div>
      <p>Count: {count}, Flag: {flag.toString()}</p>
      <button onClick={handleUpdate}>Update</button>
    </div>
  );
}

Introducing Batching Behavior

React 18 introduced batching to prevent these issues. Batching means that React groups multiple state updates into a single re-render cycle, improving performance by reducing the number of DOM manipulations. This approach ensures that the UI is updated efficiently, reflecting all state changes in one go.

function BatchingExample() {
  const [count, setCount] = useState(0);
  const [flag, setFlag] = useState(false);

  const handleUpdate = () => {
    // React batches these state updates into
    // a single re-render for better performance
    setCount(count + 1); // First update
    setFlag(!flag);      // Second update
    // Both state updates are batched and applied together,
    // resulting in a single render
  };

  console.log('Component renders');

  return (
    <div>
      <p>Count: {count}, Flag: {flag.toString()}</p>
      <button onClick={handleUpdate}>Update</button>
    </div>
  );
}

How can batching become problematic?

Consider a scenario where you're building a chat application. When a new message arrives, you want to update the message list and automatically scroll to the bottom.

Here's the challenge: if these updates are batched together, the scroll might happen before the message list is fully updated, leading to incorrect scroll behavior.

flushSync Comes to the Rescue

flushSync offers a solution to this problem. It allows you to opt-out of batching for specific updates, forcing them to be processed immediately. This ensures that critical updates, like our scroll operation, are executed in the correct order, even within a batched state update cycle.

import { useState, useRef, useEffect, flushSync } from 'react';

function ChatApp() {
  const [messages, setMessages] = useState([]);
  const endOfMessagesRef = useRef(null);

  // Simulate receiving a message
  const receiveMessage = () => {
    const newMessage = `Message ${messages.length + 1}`;

    // Update messages
    // Forces re-render
    flushSync(() => {
      setMessages(prevMessages => [...prevMessages, newMessage]);
    });

    // Scroll to the bottom after messages update
    // Message update is included in the "batch" update
    // The batch update could include other state updates (not here)
    endOfMessagesRef.current?.scrollIntoView({ behavior: "smooth" });
  };

  useEffect(() => {
    // Auto-scroll to bottom on initial render or when receiving new messages
    endOfMessagesRef.current?.scrollIntoView({ behavior: "smooth" });
  }, [messages]);

  return (
    <div style={{ height: '300px', overflow: 'auto' }}>
      <ul>
        {messages.map((message, index) => (
          <li key={index}>{message}</li>
        ))}
        {/* Marker for scrolling to the bottom */}
        <div ref={endOfMessagesRef} />
      </ul>
      <button onClick={receiveMessage}>Receive Message</button>
    </div>
  );
}

When to use it

flushSync is great for situations where update timing is important, like making sure our chat app runs smoothly. It skips React's batching and applies specific updates right away. But, use it carefully and not too much, because using it too often can cancel out the performance advantages of batching.

Make sure you actually have a problem before using it!!

Conclusion

Batching in ReactJS improves performance by combining multiple state updates into one re-render cycle. flushSync enables immediate updates when needed but should be used carefully to balance performance and functionality. UsingflushSynccorrectly results in faster, more responsive React apps, enhancing user experience and performance.

Read more here.