21
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>
);
}
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>
);
};
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.
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]);
OR
useEffect(() => {
const lastTodo = listRef.current.lastElementChild;
lastTodo.scrollIntoView();
}, [todos]);
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
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;
};
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.
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;
};
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 ✌
21