Created
September 3, 2025 01:51
-
-
Save markmals/f2d3965e6244469fa88d542e897e1649 to your computer and use it in GitHub Desktop.
Microstore using the same API as React's upcoming store API: https://github.com/facebook/react/pull/33215
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { createStore, useStore } from "./store"; | |
type CountStore = { count: number; doubled: number }; | |
const counter = createStore<CountStore, number>( | |
{ count: 0, doubled: 0 }, | |
(_prev, next) => ({ | |
count: next, | |
doubled: next * 2, | |
}), | |
); | |
export function Counter() { | |
const store = useStore(counter); | |
const increment = () => counter.update(store.count + 1); | |
return ( | |
<div> | |
<div> | |
{store.count} × 2 = {store.doubled} | |
</div> | |
<button type="button" onClick={increment}> | |
Increment | |
</button> | |
</div> | |
); | |
} | |
type CountersAction = "increment" | "decrement"; | |
const counters = createStore<number[], CountersAction>( | |
[1, 2, 3, 4, 5], | |
(prev, action) => { | |
switch (action) { | |
case "increment": { | |
const next = prev[prev.length - 1] ? prev[prev.length - 1] + 1 : 1; | |
return [...prev, next]; | |
} | |
case "decrement": { | |
return prev.slice(0, -1); | |
} | |
} | |
}, | |
); | |
export function Counters() { | |
const countArray = useStore(counters); | |
const increment = () => counters.update("increment"); | |
const decrement = () => counters.update("decrement"); | |
return ( | |
<> | |
<h1>Counters</h1> | |
{countArray ? ( | |
countArray.map((counter) => <Counter key={counter} />) | |
) : ( | |
<i>No Counters</i> | |
)} | |
<button type="button" onClick={increment}> | |
Add More Counters | |
</button> | |
<button type="button" onClick={decrement}> | |
Remove Counters | |
</button> | |
</> | |
); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { useCallback, useMemo, useSyncExternalStore } from "react"; | |
const MICRO_STORE = Symbol("MICRO_STORE"); | |
class MicroStore<Value, Action> extends EventTarget { | |
#value: Value; | |
#reducer?: (previousValue: Value, action: Action) => Value; | |
constructor( | |
initialValue: Value, | |
reducer?: (previousValue: Value, action: Action) => Value, | |
) { | |
super(); | |
this.#value = initialValue; | |
this.#reducer = reducer; | |
} | |
get value(): Value { | |
return this.#value; | |
} | |
update(action: Action) { | |
const newValue = this.#reducer | |
? this.#reducer(this.#value, action) | |
: (action as unknown as Value); | |
if (newValue !== this.#value) { | |
this.#value = newValue; | |
this.dispatchEvent(new CustomEvent("update", { detail: this.#value })); | |
} | |
} | |
} | |
/** | |
* The return value of `createStore`. | |
*/ | |
// biome-ignore lint/correctness/noUnusedVariables: Type-only | |
export interface Store<out Value, in Action> { | |
// private brand because only values from `createStore` are useable not | |
// arbitrary objects matching the shape. | |
[MICRO_STORE]: never; | |
update: (action: Action) => void; | |
} | |
type InternalStore<Value, Action> = Store<Value, Action> & { | |
_subscribe(callback: () => void): () => void; | |
_getSnapshot(): Value; | |
}; | |
export function createStore<Value>(initialValue: Value): Store<Value, Value>; | |
export function createStore<Value>( | |
initialValue: Value, | |
reducer: (previousValue: Value) => Value, | |
): Store<Value, void>; | |
export function createStore<Value, Action>( | |
initialValue: Value, | |
reducer?: (previousValue: Value, action: Action) => Value, | |
): Store<Value, Action>; | |
export function createStore<Value, Action>( | |
initialValue: Value, | |
reducer?: (previousValue: Value, action: Action) => Value, | |
): Store<Value, Action> { | |
const microStore = new MicroStore(initialValue, reducer); | |
const store: InternalStore<Value, Action> = { | |
[MICRO_STORE]: undefined as never, | |
update(action: Action) { | |
microStore.update(action); | |
}, | |
_subscribe(callback) { | |
const handleUpdate = () => callback(); | |
microStore.addEventListener("update", handleUpdate); | |
return () => microStore.removeEventListener("update", handleUpdate); | |
}, | |
_getSnapshot: () => microStore.value, | |
}; | |
return store; | |
} | |
export function useStore<Value, Action>(store: Store<Value, Action>): Value { | |
const internalStore = useMemo( | |
() => store as InternalStore<Value, Action>, | |
[store], | |
); | |
const getSnapshot = useCallback( | |
() => internalStore._getSnapshot(), | |
[internalStore], | |
); | |
const subscribe = useCallback( | |
(callback: () => void) => internalStore._subscribe(callback), | |
[internalStore], | |
); | |
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment