Prevent Screen Tearing for React 18 in a Zustand-like App with useSyncExternalStore

Daishi Kato
InstructorDaishi Kato
Share this video with your friends

Social Share Links

Send Tweet

Screen tearing is an issue that can crop up when you introduce concurrent rendering into your application. With React 18 coming out, you'll want to know how to handle this issue.

The React team has given us a solution for tearing with useSyncExternalStore. When we import his, we just need to pass our store's subscribe and selector functions to solve this issue. The added benefit here is that we can remove our uses of useCallback throughout our application.

Instructor: [0:00] We have createStore function and useStore hook as a library call. Our counter app is built on them. We have two counts in our store, and clicking the button will increment one of the counts.

[0:13] Everything works fine. There are no obvious issues, except for a very little issue for upcoming React 18. With new concurrent rendering, it can cause tearing on mount. Tearing is a video inconsistency that users can see on screen. While it's rare that tearing on mount becomes a real problem, React provides a new hook to solve this problem. The new hook is called useSyncExternalStore.

[0:40] React also provides the hook in an NPM package that works both with React 18 and pre-18. Let's modify our useStore to use the new hook. We first import useSyncExternalStore. We use the package version because we want to make it work with React 17.

[0:58] In the useStore hook, we use useSyncExternalStore. It requires two arguments. The first one is a subscribe function, and we pass store.subscribe. The second one is called getSnapshot function. This function should return a stable, immutable value.

[1:16] Because our createStore function is based on immutable data, we simply pass a function that invokes the selector on store.getState. As a result, the call is much simplified.

[1:29] One of the benefits of using useSyncExternalStore is that it accepts an inline getSnapshot function. This means we can remove useCallback hooks when using useStore.

[1:40] Let's remove those from the application code. We had four places using useCallback for selectors. Let's check the behavior. It works fine.

[1:56] The useSyncExternalStore simplifies our library code and will resolve issues in React 18. It also allows inline selectors, so it's likely that this is going to be a best practice moving forward.

[2:14] A small note on inline selectors is if we use stable selectors, React can do further optimization. In terms of our library code, we still want to have a stable getSnapshot function if our selector is stable. Although it's uncertain how the final performance would change, it looks good as a library code.

[2:35] The revision is simple. Wrap the getSnapshot function with useCallback and add dependencies properly.