Implementing a tiny global store in React

Have you ever wondered how state management libraries like Zustand or Jotai work under the hood? These libraries are incredibly useful for sharing global state across your application without the complexity of prop drilling or external context providers.
In this article you will learn how to implement a small replica of a global store using the observer pattern. This pattern allows different subscribers to listen to changes in a central piece of state and be notified whenever that state is updated.You can learn more about the observer pattern here.
The interface for tiny store is a simple one,
interface TinyStore<T> {
get: () => T;
set: (value: T) => void;
subscribe: (callback: () => void) => UnsubscribeFn;
use: () => T;
}
type UnsubscribeFn = () => void
The get
function will return the current value of the store, it can be called from anywhere, inside a React component or outside of it, but it will not trigger a re-render if used inside a React component. The set
function will update the current value in the store and notify the subscribers. The subscribe
function will allow listening for changes to the value of the store, it also returns an unsubscribe
function that you can use to stop listening for changes. The use
function is a React hook to get the value of a store in a React component.
Now, let write the function that will create the store, we will call it createTinyStore
, it will take it an initial value and it will return an object of matching interface TinyStore
.
function createTinyStore<T>(initialValue: T): TinyStore<T> {
const state = { value: initialValue }
const setState = (newValue: T) => {
state.value = newValue
notify()
}
const subscribers = new Map<symbol, () => void>()
const notify = () => subscribers.forEach(subscriber => subscriber())
const subscribe = (callback: () => void) => {
const key = Symbol()
subscribers.set(key, callback)
return () => {
subscribers.delete(key)
}
}
const useHook = () => useSyncExternalStore(subscribe, () => state.value)
return {
get: () => state.value,
set: setState,
subscribe,
use: useHook,
}
}
The hook implementation uses React’s useSyncExternalStore
, you can read more about it here. This will allow you to access this from a React component and trigger re-rendering when the value of the store changes. I will explain how the useSyncExternalStore
works in an upcoming article, follow this blog to get notification when I publish it.
Here is how you might use this in your app with a simple counter example
// src/stores/counter.ts
export const counterStore = createTinyStore(0);
// src/components/CounterDisplay.tsx
export function CounterDisplay() {
const count = counterStore.use();
return <h1>Count: {count}</h1>;
}
// src/components/CounterButtons.tsx
export function CounterButtons() {
const count = counterStore.get
const increment = () => counterCounter.set(count() + 1);
const decrement = () => counterCounter.set(count() - 1);
return (
<div>
<button onClick={increment}>Increment</button>
<button onClick={decrement}>Decrement</button>
</div>
);
}
// src/App.tsx
function App() {
return (
<>
<CounterDisplay />
<CounterButtons />
</>
);
}
You can test this out below.
By using the observer pattern and the useSyncExternalStore
hook, we've now built an atomic state management solution that efficiently manages and distributes state changes to any component that needs it. This provides a solid foundation for understanding how modern global state libraries function.
Tiny store is usable as it is, but you can expand on it by adding more functionalities. For example, you could add support for selectors to compute derived state, or build a layer for persistence to save state to local storage. You could also explore implementing Redux-style event dispatches with a state reducer for more complex applications.
Further reading
Subscribe to my newsletter
Read articles from Olaitan Ibrahim directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
