useSyncExternalStore Beyond React
useSyncExternalStore: A Bridge for External State
React’s useSyncExternalStore hook is a powerful tool for integrating external state management solutions into your React applications. While it’s often discussed in the context of React state, its real magic lies in its ability to synchronize React components with any store that follows a specific pattern, even if that store isn’t built with React in mind.
Think about it. We often have existing JavaScript codebases, or maybe we’re using libraries like Zustand, Redux, or even a simple vanilla JavaScript store. How do we make sure our React components stay up-to-date with these outside sources of truth without a lot of boilerplate or complex subscriptions? That’s where useSyncExternalStore shines.
The Core Idea: Subscribe and Get Snapshot
The hook takes two arguments: a subscribe function and a getSnapshot function.
subscribe: This function is responsible for registering a callback with the external store. Whenever the store’s state changes, it calls this callback. TheuseSyncExternalStorehook will then re-run your component’s render logic.getSnapshot: This function simply returns the current state from the external store. This is whatuseSyncExternalStorecalls to get the value to render.
This pattern is incredibly flexible. It decouples your React components from the internal workings of your state management solution. As long as your store can provide these two operations, useSyncExternalStore can work with it.
A Simple Vanilla JavaScript Store Example
Let’s build a small, plain JavaScript store and then connect it to React using useSyncExternalStore.
let count = 0;const listeners = new Set();
const counterStore = { getState() { return count; }, increment() { count += 1; listeners.forEach(listener => listener()); }, subscribe(listener) { listeners.add(listener); // Return an unsubscribe function return () => { listeners.delete(listener); }; }};
export default counterStore;This counterStore is a completely vanilla JavaScript object. It has getState, increment, and subscribe methods. The subscribe method returns an unsubscribe function, which is crucial for useSyncExternalStore to clean up listeners when a component unmounts.
Integrating with React
Now, let’s use useSyncExternalStore in a React component to display and update this counter.
import React from 'react';import useSyncExternalStore from 'react'; // In newer React versions, it's directly availableimport counterStore from '../stores/counterStore';
function CounterDisplay() { const count = useSyncExternalStore( counterStore.subscribe, counterStore.getState );
return ( <div> <h2>Counter Value: {count}</h2> <button onClick={() => counterStore.increment()}> Increment from React </button> </div> );}
export default CounterDisplay;That’s it! We’re passing counterStore.subscribe and counterStore.getState directly to useSyncExternalStore. When counterStore.increment() is called, the listeners.forEach(listener => listener()) in the store triggers the callback registered by useSyncExternalStore. This causes the hook to re-invoke counterStore.getState, get the new count, and your React component re-renders automatically.
Benefits of This Approach
- Decoupling: Your React components don’t need to know how the state is managed, only that they can subscribe to it and get its current value.
- Reusability: You can use the same non-React state logic across different frameworks or even in vanilla JavaScript applications.
- Performance:
useSyncExternalStoreis optimized to prevent unnecessary re-renders. It only subscribes and re-renders when the external store actually notifies it of a change. - No Boilerplate: Compared to manual subscription management (adding listeners in
useEffectand removing them in the cleanup), this hook drastically reduces repetitive code.
Conclusion
useSyncExternalStore is more than just a hook for integrating Redux or Zustand. It’s a fundamental building block for connecting React with any observable state source. By understanding its subscribe-and-get-snapshot pattern, you can elegantly integrate existing JavaScript state logic into your React applications, leading to cleaner, more maintainable, and performant code. It’s a testament to React’s flexibility and its ability to play well with the broader JavaScript ecosystem.