@byrding/react
v0.1.1
Published
React adapter for @byrding/core — defineStore returns a hook via useSyncExternalStore
Readme
@byrding/react
React adapter. Turns a store definition into a React hook.
AI agents — see the consumer agent guidance for best practices, patterns, and gotchas.
Install
npm install @byrding/react
# or
npx jsr add @byrding/reactdefineStore(id, definition)
function defineStore<T extends Record<string, unknown>>(
id: string,
definition: (new () => T) | (() => T),
): (keyPaths?: string[]) => TReturns a hook. The hook can be called inside any React component.
// stores/counter.ts
import { defineStore } from '@byrding/react'
export const useCounterStore = defineStore('counter', () => {
const store = {
count: 0,
get double() { return store.count * 2 },
increment() { store.count++ },
}
return store
})Using the hook
// full subscription — re-renders on any mutation
const store = useCounterStore()
// selective subscription — re-renders only when count changes
const store = useCounterStore(['count'])The return value is the live merged store object:
store.count // number — live read
store.double // number — getter called per read
store.increment() // void — action
store.count = 10 // allowed — triggers notificationAction references are stable across re-renders. Passing store.increment as a prop or onClick handler is safe and does not cause spurious re-renders.
How it works
The hook is built on useSyncExternalStore:
useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot)subscribeis stabilised withuseRef— a new function identity on every render would cause an infinite re-subscribe loop.getSnapshotreturns a cached plain object (shallow copy of_raw). The cache is invalidated on every mutation, so React sees a fresh reference and schedules a re-render.- After the render, the component reads live values from the returned merged store — so computed getters always return the current value, even if the snapshot only carries raw state.
Component ID
Each component instance is assigned a stable componentId (byrding_NN) on first render, stored in a useRef. This is what the core's subscription map uses to route notifications. You never see it.
Typing
defineStore<T> infers T from the definition:
const useCounterStore = defineStore('counter', class {
count = 0
increment() { this.count++ }
})
// => (keyPaths?: string[]) => { count: number; increment: () => void }TypeScript sees all fields on T. keyPaths is currently typed as string[] — narrow-typed key paths are a future enhancement.
Peer dependencies
react >= 18(needsuseSyncExternalStore)
