Replacing Context API and State Management Libraries with useSyncExternalStore
In React apps, managing state across several components is essential to a seamless user experience. While the Context API is a common tool for managing global states, it often leads to unnecessary re-renders. When a context value changes, all components using it useContext
to consume the context will re-render, even if the specific value they depend on hasn’t changed.
Fortunately, React introduced the useSyncExternalStore
hook, which provides a more efficient way to subscribe to changes from an external store. By using useSyncExternalStore
with a global store, we can ensure that only the relevant components re-render when the corresponding state updates.
In this blog, we will walk through the differences between using the Context API and useSyncExternalStore
for state management and demonstrate how the latter helps prevent unnecessary re-renders. Let’s dive into the code!
The Problem with Context API
In a simple application, let’s assume we have two components: one that displays the temperature and another that allows users to pick a color. Both values are managed using the Context API.
import { createContext, useContext, useState } from "react";
// Create context
const AppContext = createContext();
function App() {
const [temperature, setTemperature] = useState(22);
const [color, setColor] = useState("#ff0000");
return (
<AppContext.Provider
value={{
temperature,
color,
setTemperature,
setColor,
}}
>
<TemperatureDisplay />
<ColorPicker />
</AppContext.Provider>
);
}
function TemperatureDisplay() {
const { temperature, setTemperature } = useContext(AppContext);
const increaseTemp = () => {
setTemperature(temperature + 1);
};
return (
<div>
<h3>Temperature: {temperature}°C</h3>
<button onClick={increaseTemp}>Increase Temperature</button>
</div>
);
}
function ColorPicker() {
const { color, setColor } = useContext(AppContext);
const setColorValue = (e) => {
setColor(e.target.value);
};
return (
<div>
<h3>Selected Color: {color}</h3>
<input type="color" value={color} onChange={setColorValue} />
</div>
);
}
Here, TemperatureDisplay
and ColorPicker
both consume the same context. If either the temperature or color changes, both components will re-render because they are accessing the same context, even though the other component doesn't rely on the changed value.
Introducing useSyncExternalStore
To avoid unnecessary re-renders, we can replace the Context API with a global store approach using useSyncExternalStore
. This hook was introduced to allow React components to subscribe to external stores, and it offers fine-grained control over which components re-render when the store’s state changes.
By using useSyncExternalStore
, we ensure that components only re-render when the specific part of the state they are subscribed to changes.
Let’s implement this more efficient pattern!
Setting Up a Global Store
We will create a simple global store using a custom createStore
function that manages state and allows components to subscribe to specific parts of the store.
store.js
const createStore = (initialState) => {
let state = initialState;
const listeners = new Set();
const setState = (fn) => {
state = fn(state);
listeners.forEach((listener) => listener());
};
const getSnapshot = () => state;
const subscribe = (listener) => {
listeners.add(listener);
return () => {
listeners.delete(listener);
};
};
const getServerSnapshot = () => state;
return {
setState,
getSnapshot,
subscribe,
getServerSnapshot,
};
};
export default createStore;
The createStore
function allows us to initialize a store with initialState
and manage the updates. It also lets components subscribe to state changes via a listener
.
globalStore.js
We create an instance of the store with the initial values for temperature and color.
import createStore from "./store";
const store = createStore({
temperature: 22,
color: "#ff0000",
});
export default store;
Using useSyncExternalStore
to Subscribe to the Store
Next, we use useSyncExternalStore
to allow components to subscribe to the part of the global store they care about. This hook solves our re-render problem by ensuring that only the component that depends on the updated part of the state re-renders.
useStore.js
import { useSyncExternalStore } from "react";
const useStore = (store, selector) =>
useSyncExternalStore(
store.subscribe,
() => selector(store.getSnapshot()),
() => selector(store.getServerSnapshot())
);
export default useStore;
In useStore
, the useSyncExternalStore
hook subscribes to the store and uses a selector function to allow components to access the specific part of the store they need.
Updating Components
Now, we will update the components to use useStore
instead of useContext
to access the global store.
TemperatureDisplay.jsx
import useStore from "./useStore";
import store from "./globalStore";
const TemperatureDisplay = () => {
const temperature = useStore(store, (state) => state.temperature);
const increaseTemp = () => {
store.setState((prev) => ({
...prev,
temperature: prev.temperature + 1,
}));
};
return (
<div>
<h3>Temperature: {temperature}°C</h3>
<button onClick={increaseTemp}>Increase Temperature</button>
</div>
);
};
export default TemperatureDisplay;
ColorPicker.jsx
import useStore from "./useStore";
import store from "./globalStore";
const ColorPicker = () => {
const color = useStore(store, (state) => state.color);
const setColor = (e) => {
store.setState((prev) => ({
...prev,
color: e.target.value,
}));
};
return (
<div>
<h3>Selected Color: {color}</h3>
<input type="color" value={color} onChange={setColor} />
</div>
);
};
export default ColorPicker;
The Key Difference
Using useSyncExternalStore
ensures that only the relevant component re-renders when its associated value changes.
When the temperature is increased, only the
TemperatureDisplay
component re-renders, while theColorPicker
remains unchanged.Similarly, when the color is updated, only the
ColorPicker
component re-renders.
This is a huge improvement over the Context API, where both components would re-render regardless of whether the updated value was relevant to them or not.
// With Context API (both components re-render):
<TemperatureDisplay /> // ✅
<ColorPicker /> // ⚠️ Unnecessary re-render
// With useSyncExternalStore (only relevant component re-renders):
<TemperatureDisplay /> // ✅
<ColorPicker /> // ❌ No re-render
Conclusion
By using useSyncExternalStore
, we can prevent unnecessary re-renders, improving performance in React applications. This pattern is especially beneficial in applications with complex state management needs, as it allows components to subscribe only to the parts of the global state they depend on. It’s a powerful alternative to the Context API and third-party state management libraries, ensuring your React apps remain efficient and responsive.
Subscribe to my newsletter
Read articles from Smit Khanpara directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by