How to Secure useSyncExternalStore in React
The useSyncExternalStore hook is a powerful tool in
React for subscribing to external data sources, but improper
implementation can lead to memory leaks, performance bottlenecks, and
security vulnerabilities. This article explains how to secure your
implementation of useSyncExternalStore by ensuring proper
subscription cleanup, maintaining referential stability, validating
external data, and safely handling server-side rendering (SSR).
1. Prevent Memory Leaks with Proper Subscription Cleanup
The subscription function passed to useSyncExternalStore
must return a cleanup function. If you fail to clean up the
subscription, the callback remains registered in the external store.
This leads to memory leaks and unexpected behavior when components mount
and unmount.
// Secure implementation with cleanup
const subscribe = (callback) => {
externalStore.subscribe(callback);
// Always return a cleanup function
return () => {
externalStore.unsubscribe(callback);
};
};2. Maintain Referential Stability of Callbacks
React uses the identity of the subscribe and
getSnapshot functions to determine if it needs to
resubscribe or re-evaluate the store. If you pass inline anonymous
functions, React will resubscribe on every single render. This causes
severe performance degradation and can trigger infinite render
loops.
To secure your performance and application stability: * Declare
subscribe and getSnapshot outside the
component if they do not depend on component props. * If they depend on
props or state, wrap them in useCallback or use a memoized
selector.
// Secure: Functions declared outside the component
const subscribe = (callback) => {
window.addEventListener('online', callback);
return () => window.removeEventListener('online', callback);
};
const getSnapshot = () => navigator.onLine;
function ConnectionStatus() {
const isOnline = useSyncExternalStore(subscribe, getSnapshot);
return <div>Status: {isOnline ? 'Online' : 'Offline'}</div>;
}3. Sanitize and Validate External Store Data
Because the data source is “external” (such as
localStorage, a WebSocket, or a global window object), it
can be manipulated by malicious actors or browser extensions. Treating
this data as trusted without validation can expose your application to
Cross-Site Scripting (XSS) or prototype pollution.
Always validate and sanitize the data inside getSnapshot
before returning it to React.
import { z } from 'zod';
// Define a schema to validate the external data
const UserThemeSchema = z.union([z.literal('light'), z.literal('dark')]);
const getSnapshot = () => {
const rawData = localStorage.getItem('theme');
// Validate and provide a fallback if the data is tampered with
const result = UserThemeSchema.safeParse(rawData);
return result.success ? result.data : 'light';
};4. Prevent Hydration Mismatches in Server-Side Rendering (SSR)
If your application uses SSR (like Next.js or Remix), the client-side
state of an external store (like window.innerWidth) might
differ from the server-rendered HTML. This causes hydration mismatches,
which can break the UI or create visual bugs.
To secure the hydration process, always provide the third argument:
getServerSnapshot. This function should return a static,
safe default value that matches what the server rendered.
const subscribe = (callback) => {
window.addEventListener('resize', callback);
return () => window.removeEventListener('resize', callback);
};
// Client snapshot
const getSnapshot = () => window.innerWidth;
// Server snapshot (safe fallback for SSR)
const getServerSnapshot = () => 1024;
function ResponsiveComponent() {
const width = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
return <div>Viewport Width: {width}px</div>;
}