Library / SDK
pmndrs/valtio avatar
pmndrs/valtio

Valtio: mutable proxy state for React and vanilla JavaScript

đź§™ Valtio makes proxy-state simple for React and Vanilla

10,240 stars296 forksTypeScriptMIT

At a glance

What is it?
Valtio wraps a plain object in a self-aware proxy so you can mutate state from anywhere and let components subscribe to just the parts they read. It is small, MIT licensed, and aimed at teams who find reducers and immutable updates more ceremony than they want.
Who is it for?
Adopt Valtio if your team already thinks in plain mutable objects and wants component-level read tracking without writing reducers or selectors. Skip it if you need a state container with a documented persistence layer, devtools timeline, or middleware pipeline out of the box, because the README documents none of those and points elsewhere for persistence.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 1 day ago.
What is it written in?
Mainly TypeScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on September 30, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The problem Valtio solves: state you mutate instead of copy

Most React state libraries ask you to describe a transition rather than perform one. You write a reducer, return a new object, and let a selector decide which components care. Valtio inverts that. The README's framing is blunt: `npm install valtio` makes proxy-state simple. You pass an object to `proxy()` and from then on you change its fields directly, the same way you would with any JavaScript object.

The audience is narrower than the tagline suggests. It is for React and vanilla JavaScript developers who already have a mental model of mutable objects and do not want to unlearn it. The repository ships a `examples/photo-booth-vanillajs/` directory alongside `examples/counter/` and `examples/todo/`, which tells you the non-React path is treated as a first-class target rather than a compatibility shim. If your codebase is mostly React but has a canvas, a worker, or a game loop that also needs the same state, that split matters.

What it is not is a framework. There is no store registry, no provider at the root, no action dispatcher. The state object is the store.

How the proxy tracks writes and reads

Two different proxies do two different jobs, and the README is explicit about the split. `proxy()` from `valtio/vanilla` handles mutation tracking, meaning it notices when you write. `createProxy()` from the `proxy-compare` package handles usage tracking, meaning it notices when a component reads. Valtio depends on proxy-compare for the read side.

The write proxy is the object you import and mutate. The read proxy appears when you call `useSnapshot(state)` inside a component. According to the README, `useSnapshot` calls `snapshot` from `valtio/vanilla` and then wraps the result in another proxy that records property access. That second wrapper is why a component re-renders when `state.count` changes but not when `state.text` changes. The README's own example comments this behaviour directly.

The rule of thumb the documentation gives is short: read from snapshots in the render function, otherwise use the source. Mutations always go to the source object, never to the snapshot. That is the whole data flow. There is no diffing pass and no selector function to write.

The cost of this design shows up in the type system. The snapshot is deeply read-only, and the README acknowledges the returned type can be too strict for some uses, pointing to issue #327 and offering a module augmentation as a workaround. If you have ever fought a `readonly` modifier on a nested array, that note is for you.

Installing Valtio and writing a first component

Installation is a single npm command, and the README states it in the badge line rather than a separate section.

bash
npm install valtio

The package.json declares `typesVersions` with a `>=4.5` branch, so TypeScript 4.5 or newer is the supported floor. Older compilers are routed to a `ts_version_4.5_and_above_is_required.d.ts` stub instead of real types.

The first real use is a proxy plus a snapshot. This is the README's counter example, unchanged:

jsx
import { proxy, useSnapshot } from 'valtio'

const state = proxy({ count: 0, text: 'hello' })

function Counter() {
  const snap = useSnapshot(state)
  return (
    <div>
      {snap.count}
      <button onClick={() => ++state.count}>+1</button>
    </div>
  )
}

Read `snap.count` in the JSX and write `state.count` in the handler. The component re-renders on count changes and ignores text changes because it never touched `snap.text`. Nothing else needs wiring: no provider, no context, no store creation call.

If you want to change state from outside React, the README shows the same object being mutated on a timer:

jsx
setInterval(() => {
  ++state.count
}, 1000)

That is the pattern to remember. The proxy is not owned by React, so a websocket handler or a vanilla module can increment the same counter.

The README also recommends `eslint-plugin-valtio` for newcomers, which is a signal in itself: the library's authors expect people to make mistakes that a linter can catch.

Subscribing outside components, and where `this` breaks

`subscribe(state, callback)` returns an unsubscribe function, and the README shows it working on a whole state object or on a nested slice. Passing `state.obj` or `state.arr` subscribes to just that branch, so `state.obj.foo = 'baz'` fires the first subscriber and `state.arr.push('world')` fires the second. For a single primitive field, `subscribeKey(state, 'count', cb)` from `valtio/utils` is the documented option, and `watch` is offered as a third util for cases where auto-subscription on use is more convenient.

The sharpest limitation in the README concerns `this`. Methods defined on the proxy work when called on the source, because `this` points at the proxy. Called on a snapshot, they fail, because the snapshot is frozen. The README's example makes the failure concrete: `state.inc()` works, `snap.inc()` does not. The recommended fix is to avoid `this` entirely and write methods as arrow functions that close over the state variable.

This is not a bug report, it is a design consequence of freezing snapshots, and the README labels `this` usage as being for expert users. Treat that as a warning. If your state object has a dozen methods written with `this`, converting it to Valtio means rewriting all of them.

Async state, refs, and the batching escape hatch

Valtio is compatible with React 19's `use` hook, and the README demonstrates storing a promise directly in state: `proxy({ post: fetch(url).then((res) => res.json()) })`. The component reads `use(snap.post).title` and a parent `Suspense` boundary owns the fallback. React 18 users are pointed at the `react18-use` package for the same pattern.

The README is honest about the weak spot here. It states that this approach still suffers from de-opt, which prevents `useTransition` from working well, and names `use-valtio` as a third-party mitigation. That is a real constraint if you rely on concurrent rendering features, and it is the kind of note that many libraries bury.

For objects you do not want proxied, `ref()` marks them as untracked. The README's example keeps `document.body` in state without wrapping it, and notes that passing an existing Valtio proxy to `ref` returns the same proxy and globally marks it untracked, including in later proxy composition. Useful for large nested structures with accessors, though the README defers details to a separate API document.

Finally, mutations are batched by default before triggering re-renders. The README says the known case for disabling that batching is an `<input>` element, referencing issue #270. So synchronous updates exist, but they are positioned as an exception rather than the norm.

Valtio compared with Zustand and Redux

The most common comparison is Valtio versus Zustand, and the difference is structural rather than cosmetic. Zustand gives you a store created by a function, with a selector argument that decides what a component subscribes to. The subscription is explicit and string- or function-based. Valtio has no selector argument at all: `useSnapshot` returns a proxy, and the property accesses you perform during render define the subscription. You get automatic granularity, but you also get a subscription you cannot read off the page. A component's dependencies are whatever it happened to touch.

Against Redux, the gap is wider. Redux requires actions and reducers, and state transitions are values you can log and replay. Valtio has no such record. Mutations happen in place. That is the trade: less code, less traceability. If your team's debugging workflow depends on replaying a sequence of dispatched actions, Valtio removes that artifact entirely.

Against Immer, the difference is where the copying happens. Immer lets you write mutating code against a draft and produces a new immutable result. Valtio keeps the mutation live and produces read-only snapshots for consumers. One is a producer of immutable data, the other is a mutable source with tracked reads. They solve adjacent problems in opposite directions.

The repository's own examples directory is the most useful comparison aid, since `examples/todo/` and `examples/todo-with-proxyMap/` show the same application in two shapes.

Maintenance, licence, and what the docs leave out

The repository is not archived. The last push was on 2026-09-08, and the most recent release listed is v2.3.2 on 2026-05-01, following v2.3.1 on 2026-03-03 and v2.3.0 on 2026-01-01. The release cadence over that window is roughly every two months, with commits landing more often than releases.

The licence is MIT, declared in the LICENSE file at the repository root. That permits commercial use and modification with the copyright notice preserved. Nothing available discusses a contributor licence agreement, a CLA bot, or any relicensing history, so there is no basis for speculating about future licence changes. Standard caveat: this is a description of the licence text, not legal advice.

Upgrade cost is where the documentation is thinnest. The README does not document rollback, and it does not carry a migration guide for the 2.x line. The package.json `typesVersions` field is the one upgrade constraint stated in machine-readable form: TypeScript below 4.5 gets a stub file rather than types. The `sideEffects: false` flag and the dual ESM/CommonJS `exports` map mean bundlers can tree-shake, but they also mean the entry point you get depends on whether your resolver picks the `import` or `default` condition.

The README also does not document a persistence layer. Given that people search for one, the absence is worth stating plainly: persistence is not described in the README.

Editorial conclusion

Adopt Valtio if your team already thinks in plain mutable objects and wants component-level read tracking without writing reducers or selectors. Skip it if you need a state container with a documented persistence layer, devtools timeline, or middleware pipeline out of the box, because the README documents none of those and points elsewhere for persistence. Before committing, check three things: that your TypeScript version satisfies the >=4.5 requirement declared in package.json, that your components never call a method through the snapshot returned by useSnapshot, and that any async value you place in state is one you are willing to read through React 19's use hook.

Frequently asked questions

What is Valtio?

Valtio is a proxy-state library for React and vanilla JavaScript. It turns the object you pass to `proxy()` into a self-aware proxy, and `useSnapshot` creates a read-only local snapshot so a component re-renders only when the parts of state it actually reads have changed.

How does Valtio compare with Zustand?

Zustand subscriptions are chosen explicitly through a selector when you call the store hook. Valtio has no selector argument: `useSnapshot` returns a proxy, and the property accesses made during render determine what the component subscribes to, so granularity is automatic but not written down.

How does Valtio compare with Redux?

Redux records state transitions as dispatched actions and reducers, which gives you a replayable log. Valtio mutates state in place on the proxy, so there is no equivalent action record to inspect or replay.

How does Valtio compare with Immer?

Immer lets you write mutating code against a draft and returns a new immutable value. Valtio keeps the mutation live on the source object and hands consumers a frozen snapshot instead, so the two produce immutable and mutable results respectively.

How does Valtio compare with MobX?

The README does not describe MobX, so a direct comparison cannot be made from it. What the Valtio README does describe is a split between a write-tracking proxy from `valtio/vanilla` and a read-tracking proxy from `proxy-compare` created by `useSnapshot`.

Official sources

  1. License: MIT
  2. pmndrs/valtio on GitHub
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/pmndrs-valtio.svg)](https://hysenlabs.com/projects/pmndrs-valtio)