pmndrs/valtio
github.com/pmndrs/valtio ↗ · 227 files · 609 KB
npm install valtio makes proxy-state simple Wrap your state object Valtio turns the object you pass it into a self-aware proxy.
Overview
Valtio is a TypeScript state-management library for React and vanilla JavaScript applications. Developers mutate ordinary-looking proxy objects, while versioned snapshots and property-access tracking connect those changes to subscribers and React renders. This checkout also contains reactive collection utilities, Redux DevTools integration, a Next.js documentation website, and examples covering counters, todos, undo/redo, and browser-local photo persistence.
System map
The primary product is a library with separate vanilla and React runtime boundaries. Its state resides in JavaScript objects and runtime caches; persistence is supplied by consuming applications. Rollup produces publishable CommonJS, ESM, and declaration artifacts, while the documentation website and examples have separate application configurations. The inspected workflows describe validation and package publication, without establishing their execution results.
- src/index.ts re-exports vanilla and React APIs; src/utils.ts similarly combines vanilla utilities with React utilities, allowing consumers to choose narrower subpath imports.
- The vanilla runtime owns proxyStateMap, refSet, snapCache, proxyCache, and a shared versionHolder; each ProxyState contains the target, ensureVersion, and addListener.
- The React layer depends on useSyncExternalStore and proxy-compare's createProxy/isChanged, while the vanilla layer imports only proxy-compare helpers; React and its types are optional peers requiring version 18 or newer.
- Package exports select CJS .js/.d.ts artifacts by default and for react-native, or ESM .mjs/.d.mts artifacts for imports; typesVersions provides a fallback declaration for TypeScript versions below 4.5.
- Six build scripts select Rollup entrypoints; the config emits ES2018 CJS/ESM bundles and declarations, while postbuild rewrites declaration imports, copies metadata, and makes the dist manifest publishable.
- The pnpm workspace includes only the root and website; Vite examples have separate manifests, and their inspected configs enable React plugins, with the starter using the SWC variant.
- The website uses Next.js pages with a landing route and statically enumerated /docs/[...slug] pages; its scripts expose next dev/build/start, and styling uses class-based Tailwind dark mode and typography.
- Root validation scripts cover formatting, types, lint, and Vitest; release publication builds and publishes from dist, while push/PR preview publication uses pkg-pr-new with example templates.
Feature catalog
The library exposes mutable proxy state, snapshots, subscriptions, and React hooks as its primary developer-facing features. Utilities add reactive collections, cloning, normalization, effects, and debugging. Examples demonstrate complete interactions, while the website provides documentation navigation and theme controls. History and derived-state documentation also introduce external packages, whose implementations are outside this inspection.
- proxy() accepts an object or defaults to {}, reuses a cached proxy for the same base object, and wraps eligible assigned children; snapshot() and getVersion() expose current derived state and version information.
- subscribe() returns an unsubscribe function and supports batched or synchronous delivery; subscribeKey() adds Object.is filtering for a particular property, and unstable_enableOp() enables set/delete mutation records.
- useSnapshot() returns a property-tracked Snapshot<T> with an optional sync flag; useProxy() provides render-time snapshot reads and post-layout live reads, with an explicit React Compiler compatibility caveat.
- proxyMap() implements reactive lookup, mutation, iteration, size, and snapshot mutation guards; proxySet() adds reactive Set operations and union/intersection/difference predicates, while proxyMap's getOrInsert methods explicitly throw.
- ref() excludes an identity from tracking; deepClone() recursively copies ordinary containers while retaining configured references, and unstable_deepProxy() additionally converts native or proxy Maps/Sets into reactive collections.
- watch() runs an effect immediately, tracks proxies passed to get(), and manages cleanup/subscriptions, but is deprecated; devtools() connects to Redux DevTools for state inspection, ACTION updates, time travel, commit, and imported history.
- Examples expose counter increment/decrement and independent ticks, array-backed todo add/toggle/delete/filter, and Map-backed todo editing, bulk completion, three filters, and active counts; the history editor exposes undo/redo through valtio-history.
- The photo booth captures camera frames, accepts/rejects candidates, selects/deletes saved images, and persists images through localforage; the website exposes MDX docs, a mobile navigation dialog, and localStorage/system-driven dark mode.
Data flow
Object inputs become mutation-tracking proxies backed by identity caches and version propagation. Snapshot creation translates live state into cached read-only properties, and React adds access tracking before rendering. Reactive collections keep indexing state outside their proxied storage arrays. Application and website adapters then translate changes into browser storage, DOM updates, compiled documentation, or extension messages.
- proxy(baseObject) validates the object, checks proxyCache, creates and registers its proxy/state tuple, then assigns writable own values through the proxy to initialize nested tracking; this operates on the original base object.
- The set trap compares old and incoming identities, removes the previous child relationship, unwraps proxy-compare values, recursively proxies eligible replacements, installs their relationship, writes through Reflect.set, and notifies.
- notifyUpdate advances a shared version and sends listeners the new version; child listeners prepend their property to operation paths, while ensureVersion recursively checks child versions when the global check version changes.
- snapshot() obtains the target and current version; createSnapshotDefault reuses matching cached snapshots, caches new containers before traversing keys, preserves prototypes/enumerability, evaluates values through Reflect.get, and snapshots tracked children.
- useSnapshot() subscribes through useSyncExternalStore, obtains snapshots for client and server reads, reuses the previous snapshot when affected properties are unchanged outside render, and returns a proxy-compare wrapper that records future reads.
- proxyMap stores values in proxied data slots and maps keys to slot indexes in a closure; proxySet similarly indexes normalized values. Their epoch properties support structural tracking, and snapshot-index WeakMaps select historical lookup indexes.
- The photo booth loads localforage's images into camera.images, then a store subscription writes snapshot images back and redraws controls/images; the localStorage persistence recipe separately serializes application state under the key foo.
- Documentation flows from recursively filtered .md/.mdx files through slug generation, filesystem reads, gray-matter parsing, bundleMDX plugins, and emitted assets; getStaticProps supplies compiled code to getMDXComponent for browser rendering.
Behavior traces
These traces follow source execution order across library and application boundaries. Counter startup and mutation demonstrate proxy construction and React notification, while both todo examples show action-to-render paths. The photo booth provides a concrete persistence lifecycle, and the documentation route demonstrates build-time compilation followed by rendering. DevTools and watch illustrate bidirectional external messages and reactive-effect cleanup.
- Counter startup: main.tsx imports App → module evaluation calls proxy({number:0}), assigns nested ticks, and starts a 200 ms interval → createRoot renders StrictMode/App → Figure and Ticks call useSnapshot and read separate properties.
- Counter increment: ButtonUp's onClick increments state.number → the set trap installs the value and notifyUpdate advances its version → subscribe schedules a microtask → useSnapshot compares affected reads and returns the snapshot used by Figure.
- Array todo addition: AddTodoInput.handleSubmit calls actions.addTodo and clears its input → addTodo pushes a generated numeric id into store.todos → useTodos reads useSnapshot(store) and applies the active filter → TodoList maps results into TodoRow.
- Map todo addition: form submission prevents default and rejects whitespace-only input → actions.addTodo calls store.todos.set with an id and completed:false → proxyMap.set writes a data slot and increments epoch → useTodos iterates snapshot values and TodoList renders TodoItem.
- Photo persistence: window load awaits loadImages then startMediaStream → capture draws video onto canvas and assigns a WebP data URL → acceptance calls saveImage and clears the candidate → the subscription snapshots images into localforage and redraws the gallery; later loadImages restores them.
- Documentation navigation: getStaticPaths calls getAllDocs/getSlugs with fallback:false → getStaticProps joins slug segments and calls getDocBySlug/getDocsNav → bundleMDX returns code/frontmatter → Doc renders DocLayout and MDXRenderer, which constructs the compiled component.
- DevTools lifecycle: devtools checks its enablement and extension → enables operations and connects → subscription sends snapshots for non-marker changes → ACTION JSON is assigned into live state, while jump messages set a time-travel flag → returned cleanup removes both subscriptions.
- watch lifecycle: watch creates cleanup/subscription sets and invokes revalidate → revalidate runs previous cleanups and calls callback(get) → get immediately subscribes newly used proxies → the parent context is restored before awaiting returned work → obsolete subscriptions are removed; cleanup stops further reevaluation.
Testing signals
Tests and CI were inspected, not executed; no passing result or measured coverage is asserted. Vitest runs the tests directory in jsdom with React Testing Library and jest-dom support. Inspected suites cover core graph semantics, React rendering, utility behavior, regressions, and selected performance/memory contracts. This bounded inspection does not establish exhaustive coverage, and the root configuration does not directly include website or example application tests.
- Vitest aliases valtio imports to src, uses jsdom, global APIs, tests/setup.ts, and tests as its discovery directory; V8 coverage output is configured, but the inspected CI command is plain test:spec and no coverage threshold appears in this config.
- Vanilla tests specify object validation, proxy-cache identity, wrappers, child replacement, descriptors, shadowing __proto__/constructor properties, snapshot reuse/read-only properties, subscription batching, synchronous notification, and queued-callback cancellation.
- React optimization tests use render spies, act, fireEvent, and fake timers to specify unchanged-leaf suppression, unwrapping snapshots assigned outside render, non-own deletion behavior, and changing accessed-key dependencies.
- SSR tests assert renderToString output for current and nested state; async tests exercise Suspense with promise-backed values but skip those cases when React's use export is unavailable.
- Collection suites specify historical snapshot lookup and mutation rejection; deepProxy cases cover object cycles, aliasing, collection normalization, and descriptors. DevTools tests cover unsubscribe and malformed ACTION payloads, and watch regression tests cover synchronous loops and async context isolation.
- Memory tests use jest-leak-detector on simple/nested/cyclic proxies and unsubscribed state; the performance test samples 5,000 iterations at depths 4–256 and asserts a log-slope below 0.1 for cached snapshots with subscription, without providing results in this inspection.
- Main CI is configured on main pushes and opened/synchronized PRs to install, check formatting/types/lint, run specs, and build; root TypeScript includes src/tests, while root ESLint explicitly ignores examples and website.
- Additional workflows configure CJS/ESM artifact testing in development mode, React 18/19/canary/experimental compatibility, and TypeScript 4.5–5.9 type checks with compatibility patches; the production build-matrix option is commented out.
Risk map
These are source observations and explicitly labeled hypotheses, not confirmed vulnerabilities. The inspected core primarily manages in-process state; external trust boundaries appear in DevTools messages, browser camera/storage adapters, and documentation compilation. Identity, snapshot descriptors, global internal customization, and dependency-version drift are major correctness boundaries. Missing execution evidence leaves configured tests and deployment compatibility unresolved.
- DevTools is a state-write and state-export boundary: it sends snapshots to an extension and Object.assigns parsed ACTION/jump/import data into live proxies. ACTION parsing is caught, but jump-state parsing is outside that catch; enabled:true can override production-default disablement.
- The docs pipeline assumes trusted repository content: bundleMDX compiles source/imports, oEmbed processing accepts returned HTML, and getMDXComponent constructs executable components. Slugs are statically enumerated with fallback:false, so the inspected route is not an arbitrary public file-read endpoint.
- Photo-booth persistence writes are not awaited or caught in the store subscription, restored images are assigned without schema validation, and camera failure is only logged. Its startMediaStream implementation assigns srcObject without a stream-stop cleanup.
- Hypothesis: cyclic input can overflow deepClone's un-memoized recursion; self-referential Map/Set input can similarly recurse in unstable_deepProxy because collection branches run before memo lookup/registration. Inspected deepClone tests cover ordinary containers, and deepProxy cycle cases cover ordinary objects rather than cyclic collections. (inferred)
- Internal customization has runtime-wide blast radius: unstable_replaceInternalFunction replaces shared factories/comparators, unstable_enableOp changes shared operation generation, and ref(existingProxy) marks that proxy identity globally rather than just one parent property.
- Snapshot immutability has deliberate limits: properties are non-writable but configurable and containers remain extensible; ref values remain shared references. Separately, useSnapshot on a captured child continues observing that identity after its parent replaces the child.
- Documentation and application versions drift: subscribe-ops documents resolve/reject operations although the source Op type only contains set/delete; architecture notes describe promise unwrapping despite the v2 migration guide removing it. The website requests valtio ^1.5.2, and the Map todo uses legacy react-dom render with React 18 dependencies.
- Production-specific behavior has a configured validation gap: the artifact matrix includes development only and marks production cases skipped; the inspected main workflow runs root tests/build, whose type and lint boundaries exclude website/example source.
Extension points
Extensions primarily compose exported functions rather than register plugins. Utility barrels and separate package entrypoints define the public surface, while unstable internal APIs permit deeper runtime customization. Application actions, computed getters, persistence subscriptions, and external history packages provide higher-level composition. Website navigation and MDX plugin arrays offer separate documentation extension points.
- New vanilla helpers plug into src/vanilla/utils.ts, React helpers into src/react/utils.ts, and their combined exports reach valtio/utils through src/utils.ts.
- unstable_replaceInternalFunction supports wrappers around objectIs, newProxy, canProxy, createSnapshot, and createHandler; unstable_getInternalStates exposes the runtime maps, sets, and version holder.
- unstable_enableOp accepts true, false, or a custom operation factory; subscribe exposes generated operation tuples, and nested listeners prefix paths for integrations consuming mutation records.
- deepClone and unstable_deepProxy accept a getRefSet provider, allowing caller-controlled identities to bypass reconstruction; unstable_deepProxy rebuilds descriptors and prototypes before wrapping ordinary containers.
- Application actions and computed getters are natural composition points: todo actions directly mutate proxies, while class/getter examples derive values without a reducer registry; persistence can attach through subscribe.
- Undo/redo extends state through external valtio-history's proxyWithHistory wrapper; derive documentation points to external derive-valtio and recommends migration to valtio-reactive rather than adding a built-in derive implementation.
- Documentation files provide frontmatter titles/metadata, prepareDoc builds links, getDocsMap indexes navigation by each final slug segment, and getDocsNav explicitly chooses sidebar entries; equal final segments therefore share the same map key.
- Website content transforms plug into bundleMDX's remark/rehype arrays and esbuild loaders; remarkCodeSandboxURLUpdater accepts configurable query parameters. Library packaging separately extends through Rollup's entry aliases and config-* selection.
Contributor handoff
Start with the core runtime and the test that specifies the requested behavior, then inspect packaging or application adapters only when relevant. Preserve identity, version, listener, and snapshot contracts when editing the central proxy implementation. Tests and CI were inspected, not executed; commands below are future validation choices only when execution is authorized. The inventory and this investigation are bounded, and public documentation contains some version drift that must be checked against implementation.
- Read src/index.ts, src/vanilla.ts, and src/react.ts first for core changes: they establish exports, proxy/snapshot ownership, subscription timing, and React's access-tracked external-store adapter.
- For authorized future validation, start with pnpm run test:spec -- <relevant-test-file>, then test:types/test:lint; root test aggregates test:* scripts, and tests import valtio through aliases resolving to source rather than built artifacts.
- Tests and CI were inspected, not executed in this brief. Their configured discovery and commands describe intended validation; jsdom tests and renderToString assertions do not establish website/example end-to-end browser behavior.
- Preserve replacement and alias semantics: tests require replacement identity, unchanged previous objects, own-key order, retained shared members, and queued child operation delivery before parent replacement.
- Match strict TypeScript conventions, noUncheckedIndexedAccess/exactOptionalPropertyTypes, isolated declarations, explicit .ts source imports, single quotes, and no semicolons; declaration packaging rewrites source import extensions for published consumers.
- Use proxyMap/proxySet for reactive native-collection-like behavior, read snapshots and mutate live state, and account for globally marked refs and replaced child references; native Map/Set and Promise values are not recursively proxied by the default eligibility rule.
- For docs/example work, inspect that surface's manifest and runtime API usage instead of assuming the root version; maintain getDocsNav for new pages, verify v2 promise semantics against source, and recognize history as an external package.
- Packaging changes affect six entry builds and declaration patching; inspect the CJS/ESM workflow before selecting validation. CONTRIBUTING requests focused tests and discussion-led contributions, with maintainer-created PRs rather than direct contributor PRs.
Languages
| Language | Files | Share of source |
|---|---|---|
| TypeScript | 116 | 95% |
| Markdown | 43 | docs/config |
| JSON | 24 | docs/config |
| CSS | 11 | docs/config |
| YAML | 11 | docs/config |
| HTML | 7 | docs/config |
| JavaScript | 6 | 5% |
Start reading here
Probable source entrypoint: src/index.ts — inferred from the directory tree and manifest, not the upstream docs.
Source roots
src/ (15 files)
Source layout — src/
| Path | Files |
|---|---|
src/react/ | 2 |
src/vanilla/ | 8 |
src/index.ts | 1 |
src/react.ts | 1 |
src/types.d.ts | 1 |
src/utils.ts | 1 |
src/vanilla.ts | 1 |
Repository structure
| Top-level path | Files |
|---|---|
examples/ | 63 |
website/ | 46 |
tests/ | 42 |
docs/ | 37 |
src/ | 15 |
.github/ | 12 |
Docs found: CONTRIBUTING.mdREADME.mddocs/readme.md
Package
package.json — valtio v2.3.2, 40 declared dependencies.
@eslint/js@redux-devtools/extension@rollup/plugin-alias@rollup/plugin-node-resolve@rollup/plugin-replace@rollup/plugin-typescript@testing-library/jest-dom@testing-library/react@types/jsdom@types/react@types/react-dom@vitest/coverage-v8
CI & config entrypoints
README.md— Repository readmesrc/index.ts— TS entrypointexamples/todo-with-proxyMap/vite.config.ts— Vite configexamples/todo-with-proxyMap/src/App.tsx— React rootexamples/todo-with-proxyMap/src/main.tsx— Vite React entrypointexamples/todo/vite.config.ts— Vite configexamples/todo/src/App.tsx— React rootexamples/todo/src/main.tsx— Vite React entrypointexamples/starter/vite.config.ts— Vite configexamples/counter/vite.config.ts— Vite config
Structural health signals
Average score 9.5/10 across 78 analyzed source files; 0 hotspots surfaced.
website/components/layouts/DocLayout/DocLayout.tsx— 398 lines, score 6.9 (Deep nesting, High churn)website/components/layouts/Header/Header.tsx— 267 lines, score 6.9 (Deep nesting, High churn)src/vanilla.ts— 459 lines, score 7.4 (Large file, Deep nesting)
These are deterministic review leads — size, churn, and structural signals — not proof of a bug in this project.
Recent history
26d8ca8cb6e52026-10-01 — docs: remove codesandbox demo sections (#1254)6c4af4ba642b2026-09-29 — docs: update contributing PR policy (#1255)0face2ba533a2026-09-07 — test: add v2-compatible regression coverage (#1246)4689e794def72026-08-13 — docs: clarify ref semantics for existing proxies (#1234)a6ad5058579d2026-07-29 — fix(tests): add more test (#1233)