Skip to content

Instantly share code, notes, and snippets.

@LordZardeck
Created April 10, 2024 03:37
Show Gist options
  • Save LordZardeck/32d0bdf036a3fa0b84eb04160931b8bd to your computer and use it in GitHub Desktop.
Save LordZardeck/32d0bdf036a3fa0b84eb04160931b8bd to your computer and use it in GitHub Desktop.
Simple Context Store with useSyncExternalStore

Simple Context Store with useSyncExternalStore

Sometimes we don't want to prop drill or have many components where we don't know where they will exist in the component tree, but have frequent enough updates where a simple Context will cause unncessary re-renders or performance issues. By utilizing useSyncExternalStore with a simple store, we can avoid this and have only the components who care about the updates get re-rendered.

This may be similar to something like Redux or Zuestand, but for simple things, those are overkill to import an entire package for. The example I provided above can be a nice quick way of handling these occasional use cases without diving full send into a state management library.

Example Sandbox

import { PropsWithChildren, createContext, useContext, useState } from "react"
import { createStore, useStore } from "./store"
type CounterData = {count: number, someOtherValue: number}
const CounterContext = createContext(createStore())
export function useSelector<R>(selector?: (data: CounterData) => R) {
const store = useContext(CounterContext)
return useStore(store, selector)
}
export function useDispatch() {
const store = useContext(CounterContext)
return store.update.bind(store)
}
export function CounterProvider({children}: PropsWithChildren) {
const [store] = useState(createStore({
count: 0,
someOtherValue: 58
} as CounterData))
return <CounterContext.Provider value={store}>{children}</CounterContext.Provider>
}
import { PropsWithChildren, createContext, useContext, useState } from "react"
import { createStore, useStore } from "./store"
type CounterData = {count: number, someOtherValue: number}
const CounterContext = createContext(createStore())
export function useSelector<R>(selector?: (data: CounterData) => R) {
const store = useContext(CounterContext)
return useStore(store, selector)
}
export function useDispatch() {
const store = useContext(CounterContext)
return store.update.bind(store)
}
export function CounterProvider({children}: PropsWithChildren) {
const [store] = useState(createStore({
count: 0,
someOtherValue: 58
} as CounterData))
return <CounterContext.Provider value={store}>{children}</CounterContext.Provider>
}
import { useSyncExternalStore } from "react"
export type Store<T extends object | number | string | boolean> = {
subscribe(callback: Function): () => void;
update(updateData: T | ((previousData: T) => T)): void;
getSnapshot<S extends (data: T) => any>(selector?: S): S extends (data: T) => any ? ReturnType<S> : T;
}
export function createStore<T extends object | number | string | boolean>(data: T): Store<T> {
let listeners: Function[] = []
let storeData = data
function emitChanges() {
for (const callback of listeners) {
callback()
}
}
return {
subscribe(callback: Function) {
if (listeners.includes(callback)) return () => { }
listeners.push(callback)
return () => {
listeners.splice(listeners.indexOf(callback), 1)
}
},
update(updateData: T | ((previousData: T) => T)) {
storeData = typeof updateData === 'function' ? updateData(storeData) : updateData
emitChanges()
},
getSnapshot<S extends (data: T) => any>(selector?: S): S extends (data: T) => any ? ReturnType<S> : T {
return selector ? selector(storeData) : storeData
}
}
}
export function useStore<T, R>(store: Store<T>, selector?: (data: T) => R) {
return useSyncExternalStore((callback) => store.subscribe(callback), () => store.getSnapshot(selector))
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment