DEV Community

Cover image for Have you used `flushSync` in React?
Som Shekhar Mukherjee
Som Shekhar Mukherjee

Posted on • Edited on

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 ✌

Top comments (13)

Collapse
 
mohithgupta profile image
K.Mohith Gupta

Why dont you just use

setTimeout(()=> { listRef.current.scrollTop = listRef.current.scrollHeight;
}, 100)

Is this bad or any complications involved?

Collapse
 
somshekhar profile image
Som Shekhar Mukherjee

I don't see a reason why you would want to use setTimeout, this clearly a side effect and there are better options to handle side effects in React.

And also how would you know what the exact timeout value should be? Let's say if you choose 100ms (as mentioned in your example) and React does all the state update in 5ms, then the user of your app has to wait for 95ms (which is unnecessary) and suppose React takes 200ms then the whole purpose of using setTimeout gets nullified.

I've also elaborated the "Using effect hook" section, you might want to give it a read again.

Collapse
 
mohithgupta profile image
K.Mohith Gupta

Ok Bro, Why so serious. I'm a beginner! I just asked a doubt there!!
Thanks for your response though!

One more doubt I've got here, Can we use async on functoin and await on the
setTimeout(()=> { listRef.current.scrollTop = listRef.current.scrollHeight;
}, 100)

line?
Would that work? If yes, is this considered a bad practice?

Thread Thread
 
somshekhar profile image
Som Shekhar Mukherjee • Edited

No no, you got me wrong, I didn't mean to be rude, I was just trying to answer your question.

Event handlers can be async functions, no problem in that, but setTimeout doesn't return a promise so there's no point of using an await with it (setTimeout returns the timer ID which you can use to clear the timeout).

Example below shows an async handler:

const sleep = (time) =>
  new Promise((res) => setTimeout(res, time));

function Example() {
  const handleClick = async () => {
    await sleep(2000);
    console.log("button clicked");
  };

  return <button onClick={handleClick}>Click me</button>;
}
Enter fullscreen mode Exit fullscreen mode
Thread Thread
 
mohithgupta profile image
K.Mohith Gupta

No, I meant is it ok to have the code like this :

const onAdd = async (newTask) => {

await setTodos([...todos, { id: uuid(), task: newTask }]);

listRef.current.scrollTop = listRef.current.scrollHeight;
};

then the scrolling will wait till the settodo is done rght? or am I wrong?

Collapse
 
technikhil314 profile image
technikhil314

Thats actually bad but not too bad cause you are not mutationg dom tree. Assume you are mutating dom tree in setimeout then it would have became worst.
Also settimeout is not guaranteed to run exactly after 100ms it may drift all the way beyond 300ms mark (minimal threshhold to show feedback to user of their action) which would irritate user. Try putting a long empty loop after setTimeout and you can see that happening.

Collapse
 
wasim1312 profile image
wasim • Edited

const cancel = () => {
flushSync(() => {
changeModalRef.current?.close();
});

flushSync(() => {
  cancelModalRef.current?.open();
});
Enter fullscreen mode Exit fullscreen mode

};

when i am opening cancelModal focus is not setting correctly
if we use multiple flushSync inside same method. Does it work as expected

Collapse
 
jcubic profile image
Jakub T. Jankiewicz

The demo scroll Dev.to post page when click Add Task. It's worth fixing this issue.
Good article anyway.

Collapse
 
somshekhar profile image
Som Shekhar Mukherjee • Edited

Yeah I've noticed that too, but I don't know how to fix it 😅. Let me know if you have any solution to it.

Collapse
 
jcubic profile image
Jakub T. Jankiewicz • Edited

I have no idea, but from my experience if the page is iframe the only way for outer page to know what the iframe is doing is listen to post messages that are send by the codesandbox, but that's just a guess, maybe looking at forem source code will give you a clue what the page is doing. I would check if the same works with CodePen or something similar that is inside iframe.

Thread Thread
 
somshekhar profile image
Som Shekhar Mukherjee

I found why this is happening, it's because of scrollIntoView. It's scrolls the outer page as well to bring the lastTodo into view.

Swapping it with listRef.current.scrollTop = listRef.current.scrollHeight solves this. So, I guess its better to use the latter.

Collapse
 
devchester profile image
Chester Arellano

nice!

Collapse
 
devchester profile image
Chester Arellano

just keep posting with Sandbox example for readers' playground. keep it up! :D