Claude generated
Summary
useDb selectors that synthesize a new array/object shape inside the selector (rather than returning a reference owned by the replica) trigger Maximum update depth exceeded. The constraint is real, but it's invisible from the docs and the failure mode is just a stack trace deep inside react-dom, which makes it hard to diagnose.
Repro
A selector that projects into a new shape:
type Entry = { id: string; label: string }
function ShelfMenu() {
const entries = useDb<Entry[]>(root => {
const out: Entry[] = []
for (const scope of Object.values(root.app.scopes)) {
if (!scope.archived) continue
out.push({ id: scope.id, label: scope.directory })
}
return out
})
return entries.map(e => <div key={e.id}>{e.label}</div>)
}
This loops on mount with:
Error: Maximum update depth exceeded. ...
at forceStoreRerender (react-dom_client.js)
at updateStoreInstance (react-dom_client.js)
Workaround
Return raw db slices and project in useMemo:
const scopesById = useDb(root => root.app.scopes)
const entries = useMemo(
() =>
Object.values(scopesById)
.filter(s => s.archived)
.map(s => ({ id: s.id, label: s.directory })),
[scopesById],
)
What I expected
Either (a) it works (selectors are memoized / equality is structural), or (b) the framework surfaces a clear dev-mode warning instead of a generic React infinite-loop trace.
The docs at /core/state show useDb(root => root.app.todos) and useDb(root => root.app.count) — both happen to return refs the replica owns, so the constraint isn't visible from the examples. useDb(root => Object.values(root.app.scopes)) is used heavily inside our own app and works, which made me assume useDb did structural array equality. Building {...} literals inside the selector is what crosses the line.
Suggested improvements
- Document the equality contract in
/core/state. Something like: "Selectors should return values owned by the database. Projecting into new object/array shapes inside the selector will cause re-render loops — use useMemo on top."
- Dev-mode warning when a selector returns a non-equal value across consecutive renders with no intervening db update, à la React's own
getSnapshot should be cached warning for useSyncExternalStore. This is the change that would have saved the most debugging time.
- Optional: a
useDb.shallow(...) / useShallow helper for the "I want to project into a new shape" case, mirroring zustand's API so users don't have to learn the raw-slice + useMemo idiom.
Environment
@zenbujs/core@0.0.25
- React 19.2.6
Claude generated
Summary
useDbselectors that synthesize a new array/object shape inside the selector (rather than returning a reference owned by the replica) triggerMaximum update depth exceeded. The constraint is real, but it's invisible from the docs and the failure mode is just a stack trace deep inside react-dom, which makes it hard to diagnose.Repro
A selector that projects into a new shape:
This loops on mount with:
Workaround
Return raw db slices and project in
useMemo:What I expected
Either (a) it works (selectors are memoized / equality is structural), or (b) the framework surfaces a clear dev-mode warning instead of a generic React infinite-loop trace.
The docs at
/core/stateshowuseDb(root => root.app.todos)anduseDb(root => root.app.count)— both happen to return refs the replica owns, so the constraint isn't visible from the examples.useDb(root => Object.values(root.app.scopes))is used heavily inside our own app and works, which made me assumeuseDbdid structural array equality. Building{...}literals inside the selector is what crosses the line.Suggested improvements
/core/state. Something like: "Selectors should return values owned by the database. Projecting into new object/array shapes inside the selector will cause re-render loops — useuseMemoon top."getSnapshot should be cachedwarning foruseSyncExternalStore. This is the change that would have saved the most debugging time.useDb.shallow(...)/useShallowhelper for the "I want to project into a new shape" case, mirroring zustand's API so users don't have to learn the raw-slice +useMemoidiom.Environment
@zenbujs/core@0.0.25