Have you used `flushSync` in React?

somshekhar

Som Shekhar Mukherjee

Posted on December 23, 2021

Have you used `flushSync` in React?

In this post we'll discuss about the flushSync utility provided by react-dom.

Let's try and understand what flushSync is and how it can useful through an example.

As always, it's a simple todo example but the point to note here is that the todo container has fixed height and is scrollable.

So, there's our App component that has a todos state and returns a list of todos along with a form.

export default function App() {
  const [todos, setTodos] = useState(mockTodos);

  const onAdd = (newTask) => {
    setTodos([...todos, { id: uuid(), task: newTask }]);
  };

  return (
    <section className="app">
      <h1>Todos</h1>
      <ul style={{ height: 200, overflowY: "auto" }}>
        {todos.map((todo) => (
          <li key={todo.id}>{todo.task}</li>
        ))}
      </ul>
      <AddTodo onAdd={onAdd} />
    </section>
  );
}
Enter fullscreen mode Exit fullscreen mode

The AddTodo component is also fairly simple, it just manages the input state and once the form is submitted it calls the onAdd prop with the new todo.

const AddTodo = ({ onAdd }) => {
  const [taskInput, setTaskInput] = useState("");

  const handleSubmit = (e) => {
    e.preventDefault();

    if (!taskInput.trim()) return;
    setTaskInput("");
    onAdd(taskInput.trim());
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        placeholder="Your Task"
        value={taskInput}
        onChange={(e) => setTaskInput(e.target.value)}
      />
      <button>Add Task</button>
    </form>
  );
};
Enter fullscreen mode Exit fullscreen mode

Now that we've an understanding of how our code works, suppose we want to add a functionality where every time a new todo is added, the container is scrolled to its bottom so that the newly added todo is visible to the user.

Think for while and figure out how you would go about implementing this functionality.

Using useEffect hook

You might be thinking of using the effect hook. So, every time the todos change just scroll the container to the bottom.

useEffect(() => {
  listRef.current.scrollTop = listRef.current.scrollHeight;
  // listRef is simply a ref attached to the ul
}, [todos]);
Enter fullscreen mode Exit fullscreen mode

OR

useEffect(() => {
  const lastTodo = listRef.current.lastElementChild;
  lastTodo.scrollIntoView();
}, [todos]);
Enter fullscreen mode Exit fullscreen mode

Both of the above scrolling logics work fine (you might even want to use the useLayoutEffect hook in this situation in case you observe any jitters in scrolling).

But, I would not want to put this in either of these hooks, let me explain why.

The DOM manipulation (scrolling in this case) that we're trying to do here is a side effect (something that doesn't happen during rendering) and in React side effects usually happen inside event handlers, so in my opinion the best place to put this would be inside the onAdd handler.

Also, if you go by the docs useEffect should be your last resort, when you've exhausted all other options but haven't found the right event handler.

If you’ve exhausted all other options and can’t find the right event handler for your side effect, you can still attach it to your returned JSX with a useEffect call in your component. This tells React to execute it later, after rendering, when side effects are allowed. However, this approach should be your last resort. DOCS

Scrolling logic inside the handler

If you simply put the scrolling logic inside the handler (as shown below), you would notice that you're not exactly getting the desired results.

const onAdd = (newTask) => {
  setTodos([...todos, { id: uuid(), task: newTask }]);

  listRef.current.scrollTop = listRef.current.scrollHeight;
};
Enter fullscreen mode Exit fullscreen mode

Because setTodos is not synchronous, what happens is you scroll first and then the todos actually get updated. So, what's in view is not the last todo but second to last.

So, to get it working as expected we would have to make sure that the logic for scrolling runs only after the todos state has been updated. And that's where flushSync comes handy.

Using flushSync

To use flushSync, we need to import it from react-dom: import { flushSync } from "react-dom";

And now we can wrap the setTodos call inside flushSync handler (as shown below).

const onAdd = (newTask) => {
  flushSync(() => {
    setTodos([...todos, { id: uuid(), task: newTask }]);
  });

  listRef.current.scrollTop = listRef.current.scrollHeight;
};
Enter fullscreen mode Exit fullscreen mode

Now we have made sure that the state update happens synchronously and the logic for scrolling is executed only after the state has been updated.

That's it for this post, let me know situations where you would want to use flushSync.

Peace ✌

💖 💪 🙅 🚩
somshekhar
Som Shekhar Mukherjee

Posted on December 23, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related