In modern React applications, state management has moved beyond simple useState hooks. As we push for higher performance with concurrent rendering, we often integrate external stores—like Redux, Zustand, or custom event emitters—directly into the React lifecycle. However, a common frustration for senior engineers is the dreaded "tearing" effect, where the UI displays inconsistent data because the external store updated during a startTransition process.
TL;DR: React's useSyncExternalStore hook is the only safe way to bridge external state with concurrent React. If your UI is showing mismatched data during state updates, you are likely missing the getSnapshot memoization or the subscribe function is not properly handling store mutations.
Key takeaways
- Tearing occurs when React renders different parts of the tree based on different versions of an external store.
useSyncExternalStoreforces React to treat external state as a single, consistent source of truth.- Always memoize your
getSnapshotfunction to prevent unnecessary re-renders. - The
subscribefunction must return a cleanup function to prevent memory leaks in production.
The Problem: Why UI Tearing Happens
In concurrent mode, React can pause, resume, or even discard work. If an external store (like a global variable or a WebSocket-backed object) mutates while React is in the middle of rendering a component tree, different components might read different versions of that state. This is called tearing.
Before useSyncExternalStore was introduced, developers often relied on useEffect or useLayoutEffect to sync state. These approaches fail in concurrent rendering because they don't inform React that the state might change during the render phase. When we see this in production, it often manifests as flickering UI elements or "content jumping" where a user name updates before the associated ID, causing a temporary data mismatch.
Implementing useSyncExternalStore Correctly
The hook requires three arguments: subscribe, getSnapshot, and an optional getServerSnapshot. The key to avoiding the tearing issue is ensuring that getSnapshot returns an immutable value or a value that is stable between renders.
import { useSyncExternalStore } from 'react';
// Your external store
const store = {
state: { count: 0 },
listeners: new Set(),
subscribe(listener) {
this.listeners.add(listener);
return () => this.listeners.delete(listener);
},
getSnapshot() {
return this.state;
},
increment() {
this.state = { count: this.state.count + 1 };
this.listeners.forEach(l => l());
}
};
function Counter() {
const snapshot = useSyncExternalStore(
store.subscribe.bind(store),
store.getSnapshot.bind(store)
);
return <div>Count: {snapshot.count}</div>;
}
Common Pitfalls in 2026
In our recent work with high-frequency data dashboards, we observed teams making the mistake of defining the getSnapshot function inline. This causes React to think the store has changed on every render, effectively disabling the performance benefits of concurrent rendering and potentially re-triggering effects unnecessarily.
The "Inline Function" Anti-Pattern
Avoid this pattern at all costs:
// DON'T DO THIS
const snapshot = useSyncExternalStore(
subscribe,
() => store.getState() // New function reference every render!
);By passing a new function reference every time, you break the internal comparison logic React uses to determine if the state has actually changed. Always define your getSnapshot outside the component or wrap it in useCallback if it depends on component props.
Comparison: Legacy vs. Modern Synchronization
| Approach | Safety | Concurrent Support | Complexity |
|---|---|---|---|
| useEffect Sync | Low (Risk of tearing) | No | High |
| useSyncExternalStore | High (Guaranteed) | Yes | Low |
| Custom Event Listeners | Medium | No | Medium |
When NOT to use this approach
While useSyncExternalStore is powerful, it is not a silver bullet for every state problem. If your state is strictly local to a component or can be lifted into standard React state (using useState or useReducer), do not reach for an external store. Introducing an external dependency adds unnecessary abstraction and overhead. Use this hook only when you are integrating with libraries like Redux, Zustand, or custom WebSocket implementations that exist outside the React tree.
FAQ
Does useSyncExternalStore work with Server Side Rendering?
Yes, but you must provide the getServerSnapshot parameter. This ensures that the server renders the initial state correctly before the hydration process begins on the client, preventing content mismatch errors.
Why is my store tearing even with useSyncExternalStore?
Usually, this happens because the getSnapshot function returns a mutable object that you are modifying directly. Always return a new object (shallow copy) when the state updates to ensure React detects the change correctly.
Can I use this for non-React state?
Technically, yes, but it is purpose-built for React. If you need to sync state between non-React parts of your application, standard EventTarget or CustomEvent patterns are more idiomatic and performant.
Need Expert React Implementation?
Managing concurrent state transitions and preventing UI tearing requires a deep understanding of React's internal fiber architecture. If your team is struggling with complex state synchronization or performance bottlenecks, we are here to help. Hire React developers from Krapton to ensure your application is built on a solid, performant foundation. We specialize in scaling complex frontends for high-traffic environments.
Krapton Engineering
The Krapton engineering team has years of hands-on experience building high-performance web applications using React and Next.js, solving complex state management issues for startups and enterprises worldwide.



