Files
agentic-dev/docs/library-decisions/2026-07-12-zustand.md
Danijel Martinek 3a1b0886c5 docs(library-decisions): trace zustand v5 for the editor store
Pre-approve the ADR-022 library trace for the editor's client-state
store (ADR-029: store on DesignDoc v1, React 19). Evaluates v5
specifically — the founder prototype used v4. Trace only — the package
is not installed by this commit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016j8z4VHjedXDTjEDNg7qHK
2026-07-12 19:12:44 +02:00

7.7 KiB

package, version, tier, decision, date, deciders, adr, lastRevalidated, is-sub-processor, processes-pii, filter-results, verification-commands, accepted-cves
package version tier decision date deciders adr lastRevalidated is-sub-processor processes-pii filter-results verification-commands accepted-cves
zustand ^5.0.0 feature approved 2026-07-12
implementer-agent
null null false false
license types maintenance boundary-fit shadow-check eu-residency cve-scan named-consumer socketRisk
MIT native active pass pass n/a clean pass clean
npm info zustand license
npm info zustand types
cat package.json | grep -E '"(zod|inversify|payload|@trpc/server|superjson|reflect-metadata)"'
npm info zustand time.modified
npm info zustand time --json | tail -5
npm info zustand@5 dependencies peerDependenciesMeta --json
pnpm audit --audit-level=moderate 2>&1 | head -40
curl -s 'https://api.github.com/advisories?affects=zustand&per_page=10'

Filter: license

npm info zustand license returns MIT. The GitHub repository (pmndrs/zustand) confirms spdx_id: MIT. Within the allowlist.

Filter: types

Authored in TypeScript; the published manifest declares "types": "./index.d.ts". No separate @types/ package is needed (@types/react is an optional peer, only relevant to consumers already using React types).

Filter: shadow-check

zustand is a client-side React state container. It duplicates no locked must-have (zod, inversify, payload, @trpc/server, superjson, reflect-metadata — root package.json grep matches only zod): inversify is server-side DI for use-case wiring, and @tanstack/react-query (already in the workspace via the feature-UI stack) owns the server-state cache — zustand covers client-only editor state (board registry, selection), a role no workspace library fills today. One version-skew note, not a shadow: @xyflow/react depends on zustand@^4.4.0 for its internal store, so v4 will coexist in node_modules as React Flow's private implementation detail while packages/editor targets v5 directly; pnpm isolates the two majors. Pass.

Filter: boundary-fit

Target is packages/editor (feature tier — the ADR-029 editor rebuild, seeded by the walking-skeleton PRD). zustand is a pure in-process state container: zero runtime dependencies, no @sentry/* or @opentelemetry/sdk-* imports (ADR-017 §4 unaffected), no cross-feature imports. Its four peer dependencies (react, @types/react, immer, use-sync-external-store) are all optional. ADR-006, ADR-010, ADR-017 all pass.

Filter: maintenance

Latest release 5.0.14 published 2026-05-28T10:17:58.503Z — about six weeks before this trace. Steady patch cadence on the 5.x line (5.0.9 2025-11-30, 5.0.10 2026-01-12, 5.0.11 2026-02-01, 5.0.12 2026-03-16, 5.0.13 2026-05-05, 5.0.14 2026-05-28). The pmndrs/zustand repository was last pushed 2026-07-10, has ~58.6k stars and only 4 open issues, and is maintained by Poimandres (lead: Daishi Kato). ~36.7M npm downloads/week. Active.

Filter: eu-residency

Pure in-process state management. No network calls, no vendor-controlled endpoints, no telemetry. Not applicable.

Filter: cve-scan

The GitHub Advisory Database reports zero advisories for zustand (any version). The package has zero runtime dependencies, so there is no transitive advisory surface. pnpm audit --audit-level=moderate at the workspace root surfaces only pre-existing advisories in dev tooling (vitest via @stryker-mutator/vitest-runner, shell-quote via concurrently) — none related to this package. Clean.

Filter: named-consumer

packages/editor — the walking-skeleton PRD (docs/work/prds/walking-skeleton.prd.md) specifies a "minimal zustand store" that "holds registry + selection only" as the first code of the ADR-029 editor rebuild, whose store is designed around DesignDoc v1 and the iframe canvas (ADR-028). The platform-retrofit PRD explicitly batches this trace so walking-skeleton implementers do not stall on the ADR-022 pre-commit hook. The consumer is a scheduled, named package blocked today — not hypothetical.

This trace evaluates v5 specifically: the founder prototype used v4, but the rebuild targets v5 + React 19. v5 requires React >= 18 and uses React's native useSyncExternalStore (dropping v4's use-sync-external-store runtime dependency); its breaking changes versus v4 are removals of already-deprecated APIs and stricter equality semantics (useShallow for shallow-compared selectors) — compatible with the React 19 target of apps/web-next (ADR-029).

Filter: socketRisk

The official Socket CLI requires an organization API token (socket package score fails with "requires a Socket API token"), which is unavailable in this environment, and socket.dev package pages are bot-gated (HTTP 403). Manual supply-chain audit performed instead.

zustand@5.0.14 has zero runtime dependencies. All four peer dependencies (@types/react, immer, react, use-sync-external-store) are marked optional in peerDependenciesMeta. The published manifest declares no preinstall/postinstall lifecycle scripts, the package makes no network calls, and there is no minification/obfuscation in the published source. Poimandres has published the package continuously since 2019 under the same organization. Supply-chain risk assessed as clean.

Prompt: replaces

Nothing in the workspace is retired — no client-state library exists in the monorepo today (@tanstack/react-query covers server-state caching, which zustand does not touch). At the product level it carries forward the founder prototype's store choice (zustand v4) into the conformance-native rebuild (ADR-029), one major version ahead; the prototype's VeectNode store shape is explicitly not inherited — ADR-029 makes the editor store a view-model mapped to/from DesignDoc v1. What it displaces is the alternative of React context + useReducer for editor state.

Prompt: migration-cost-out

Mechanical. Store definitions are plain factory functions (create/set/get) and components consume them through hooks — all confined to packages/editor; no other feature may import across the boundary. ADR-029's mapping layer keeps DesignDoc v1 (the persisted contract) independent of the store shape, so no persisted data or protocol couples to zustand. Ripping it out means rewriting the store modules and hook call sites within one package. The single caveat: the board's high-frequency interaction paths will use zustand's transient subscriptions (subscribe without re-render), and a replacement must offer an equivalent to keep the <16 ms/frame budget (ADR-028) — a design constraint on the successor, not a lock-in.

Prompt: alternatives-considered

  1. jotai — atom-based state, also by Poimandres. Granularity mismatch: the editor store is a document/view-model store with cohesive slices (registry, selection), not a constellation of independent atoms. Also loses the alignment bonus that @xyflow/react itself runs on zustand, which keeps one mental model across the board substrate and the editor store.
  2. Redux Toolkit — mature and battle-tested, but heavier: reducer/action boilerplate for what is a single-package UI store, immer-by-default update overhead on 60 fps drag paths, and no first-class transient (non-rendering) subscription API comparable to zustand's subscribe.
  3. React context + useReducer — zero dependencies, but architecturally disqualified for a canvas editor: every dispatch re-renders all consumers, there are no transient subscriptions, and selector-based subscription would have to be hand-built — precisely the re-render storm the <16 ms/frame interaction budget (ADR-028) forbids.