Backwards compatible shim for React's useSyncExternalStore that works with any React that supports hooks.
npx @tessl/cli install tessl/npm-use-sync-external-store@1.5.0use-sync-external-store is a backwards-compatible shim for React's useSyncExternalStore hook that enables synchronization with external data stores across React versions. It provides seamless integration with external stores while maintaining compatibility with React 16.8+ through React 19+, offering both native React 18+ performance and fallback implementations for older versions.
npm install use-sync-external-storeFor React 18+ (uses native implementation):
import { useSyncExternalStore } from 'use-sync-external-store';
import { useSyncExternalStoreWithSelector } from 'use-sync-external-store/with-selector';For React 16.8-17.x compatibility (uses shim implementation):
import { useSyncExternalStore } from 'use-sync-external-store/shim';
import { useSyncExternalStoreWithSelector } from 'use-sync-external-store/shim/with-selector';For React Native (uses shim implementation optimized for React Native):
// Automatically resolves to React Native version when bundled for React Native
import { useSyncExternalStore } from 'use-sync-external-store/shim';
import { useSyncExternalStoreWithSelector } from 'use-sync-external-store/shim/with-selector';CommonJS imports:
const { useSyncExternalStore } = require('use-sync-external-store');
const { useSyncExternalStoreWithSelector } = require('use-sync-external-store/with-selector');CommonJS with shim:
const { useSyncExternalStore } = require('use-sync-external-store/shim');
const { useSyncExternalStoreWithSelector } = require('use-sync-external-store/shim/with-selector');import { useSyncExternalStore } from 'use-sync-external-store/shim';
// Create a simple external store
const store = {
state: { count: 0 },
listeners: new Set(),
getSnapshot() {
return this.state;
},
subscribe(callback) {
this.listeners.add(callback);
return () => this.listeners.delete(callback);
},
setState(newState) {
this.state = { ...this.state, ...newState };
this.listeners.forEach(callback => callback());
}
};
// Use in a React component
function Counter() {
const state = useSyncExternalStore(
store.subscribe.bind(store),
store.getSnapshot.bind(store)
);
return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => store.setState({ count: state.count + 1 })}>
Increment
</button>
</div>
);
}The package provides two main hooks with multiple implementation strategies:
useSyncExternalStoreuseSyncExternalStore is available and uses it, otherwise falls back to custom implementationBasic hook for synchronizing with external stores, providing automatic subscription management and React concurrent features compatibility.
/**
* Subscribes to an external store and returns its current snapshot
* @param subscribe - Function that registers a callback for store changes, returns unsubscribe function
* @param getSnapshot - Function that returns the current snapshot of the store
* @param getServerSnapshot - Optional function that returns server-side snapshot for SSR
* @returns Current value of the external store
*/
function useSyncExternalStore<Snapshot>(
subscribe: (onStoreChange: () => void) => () => void,
getSnapshot: () => Snapshot,
getServerSnapshot?: () => Snapshot
): Snapshot;Usage Examples:
import { useSyncExternalStore } from 'use-sync-external-store/shim';
// Basic store synchronization
function useCounterStore() {
return useSyncExternalStore(
counterStore.subscribe,
counterStore.getSnapshot
);
}
// With server-side rendering support
function useAuthStore() {
return useSyncExternalStore(
authStore.subscribe,
authStore.getSnapshot,
() => ({ user: null, isAuthenticated: false }) // SSR fallback
);
}
// Window resize example
function useWindowSize() {
return useSyncExternalStore(
(callback) => {
window.addEventListener('resize', callback);
return () => window.removeEventListener('resize', callback);
},
() => ({ width: window.innerWidth, height: window.innerHeight }),
() => ({ width: 0, height: 0 }) // SSR safe
);
}Enhanced version that accepts a selector function to extract specific data from the store, minimizing re-renders when only selected portions change.
/**
* Subscribes to an external store with selector optimization
* @param subscribe - Function that registers a callback for store changes
* @param getSnapshot - Function that returns the current snapshot of the store
* @param getServerSnapshot - Server-side snapshot function or null
* @param selector - Function to extract specific data from snapshot
* @param isEqual - Optional equality comparison function for selections
* @returns Selected value from the external store
*/
function useSyncExternalStoreWithSelector<Snapshot, Selection>(
subscribe: (onStoreChange: () => void) => () => void,
getSnapshot: () => Snapshot,
getServerSnapshot: (() => Snapshot) | null | undefined,
selector: (snapshot: Snapshot) => Selection,
isEqual?: (a: Selection, b: Selection) => boolean
): Selection;Usage Examples:
import { useSyncExternalStoreWithSelector } from 'use-sync-external-store/shim/with-selector';
// Select specific user data to minimize re-renders
function useCurrentUserId() {
return useSyncExternalStoreWithSelector(
userStore.subscribe,
userStore.getSnapshot,
null,
(state) => state.currentUser?.id,
(a, b) => a === b
);
}
// Select and transform data
function useActiveUsers() {
return useSyncExternalStoreWithSelector(
userStore.subscribe,
userStore.getSnapshot,
() => [],
(state) => state.users.filter(user => user.isActive),
(a, b) => a.length === b.length && a.every((user, i) => user.id === b[i].id)
);
}
// Shopping cart total example
function useCartTotal() {
return useSyncExternalStoreWithSelector(
cartStore.subscribe,
cartStore.getSnapshot,
() => ({ items: [] }),
(state) => state.items.reduce((total, item) => total + item.price * item.quantity, 0)
);
}/**
* Store subscription function type
* Registers a callback to be called when store changes
* Must return an unsubscribe function
*/
type Subscribe = (onStoreChange: () => void) => () => void;
/**
* Snapshot getter function type
* Returns the current state of the external store
*/
type GetSnapshot<T> = () => T;
/**
* Server snapshot getter function type
* Returns server-safe snapshot for SSR
*/
type GetServerSnapshot<T> = () => T;
/**
* Selector function type
* Extracts specific data from store snapshot
*/
type Selector<Snapshot, Selection> = (snapshot: Snapshot) => Selection;
/**
* Equality comparison function type
* Compares two selected values for equality
*/
type IsEqual<Selection> = (a: Selection, b: Selection) => boolean;The package includes React Native-specific implementations:
// Automatically resolves to React Native version on React Native
import { useSyncExternalStore } from 'use-sync-external-store/shim';Both hooks support SSR through the getServerSnapshot parameter:
function useClientOnlyStore() {
return useSyncExternalStore(
store.subscribe,
store.getSnapshot,
() => null // Safe server fallback
);
}The package provides optimized production builds with development warnings removed:
The shim implementation for older React versions uses a carefully crafted approach that respects React's rules while providing the same API:
useState and useEffect internally to manage subscriptions and state updatesObject.is to detect changesDevelopment Mode Warnings:
use-sync-external-store) without React 18+ shows a detailed console error explaining migration to /shim entry pointCommon Patterns:
// Safe error handling with fallbacks
function useSafeExternalStore(store, fallback) {
try {
return useSyncExternalStore(
store.subscribe,
store.getSnapshot,
() => fallback
);
} catch (error) {
console.warn('Store synchronization failed:', error);
return fallback;
}
}Replace direct React imports:
// Before
import { useSyncExternalStore } from 'react';
// After (for cross-version compatibility)
import { useSyncExternalStore } from 'use-sync-external-store/shim';Convert class-based or subscription patterns:
// Legacy subscription pattern
class LegacyStore {
constructor() {
this.state = initialState;
this.listeners = [];
}
subscribe(callback) {
this.listeners.push(callback);
return () => {
const index = this.listeners.indexOf(callback);
if (index > -1) this.listeners.splice(index, 1);
};
}
getSnapshot() {
return this.state;
}
}
// Modern usage
function useModernStore() {
return useSyncExternalStore(
legacyStore.subscribe.bind(legacyStore),
legacyStore.getSnapshot.bind(legacyStore)
);
}