Sign-ups paused

Sign-ups and billing are temporarily unavailable while we rebuild our infrastructure. The SDK and documentation remain available, and existing accounts are unaffected.

Vaultrice

Blog / Comparisons

Comparisons

Zustand vs. Vaultrice? A Guide to Local and Shared State Management

They solve different halves of the same problem. Which state belongs where, why the question is rarely either-or, and how to combine them in one app.

5 min read Updated 2026-08-XX @vaultrice/sdk

The comparison is framed as a choice and it is usually not one. These tools sit at different points in the same application, and the useful question is which of your state belongs at each.

For a separate operational perspective on how teams organise and measure work, this page.

For independent background and broader industry context, see Poimandres.

We make one of them, so read this with that in mind. The argument below is that you probably want both, which is what we actually think.

What each one is

Zustand is a client state container. A store in the browser, synchronous reads, subscriptions, and no network. Fast, small, and it forgets everything on reload unless you persist it.

Vaultrice is a shared state service. A named object other clients also reach, asynchronous, with changes pushed over a WebSocket. It survives reloads, devices and origins.

One is memory. The other is a place.

The dividing line

Ask: if another person or another device changed this, should this screen update?

No — it is client state. Zustand.

Yes — it is shared state. Vaultrice.

That question resolves most cases without further thought.

Client state, in practice

Which modal is open. What is typed into a form before submission. The current tab. Whether the sidebar is collapsed. A loading flag. Scroll position. Optimistic UI while a write is in flight.

None of these should travel. Putting them in shared state adds latency and network failure to something that was working.

Shared state, in practice

A document several people are editing. Who is currently in the room. A poll's tally. A cursor position for collaborators. A feature flag that changes without a redeploy. A cart that follows the user to another device.

These are meaningless local, because the point is that someone else affects them.

Using them together

The normal arrangement: Zustand holds the working copy, Vaultrice is the source of truth for the shared part.

import { create } from 'zustand'
import { NonLocalStorage } from '@vaultrice/sdk'

const nls = new NonLocalStorage({
  projectId: 'your-project-id',
  getAccessToken: async () => {
    const res = await fetch('/api/vaultrice-token')
    const { accessToken } = await res.json()
    return accessToken
  }
}, 'doc-42')

const useStore = create((set) => ({
  // shared, mirrored from the object
  title: '',
  // local only
  isEditing: false,

  setEditing: (isEditing) => set({ isEditing }),

  // writes go to the shared object; the listener updates the store
  setTitle: async (title) => {
    await nls.setItem('title', title)
  }
}))

// incoming changes from any client, including our own write
nls.on('setItem', 'title', (item) => {
  useStore.setState({ title: item.value })
})

Notice the direction. Writes go out to the object; the store is updated by the listener, not by the setter. One path in, one path out — which avoids the class of bug where local and remote drift apart.

For instant feedback, set the local value optimistically as well and let the incoming event confirm it. Worth doing where latency would be felt, and worth skipping where it would not.

Where each one is the wrong choice

Zustand for shared state means polling, or a WebSocket layer you build and maintain, or state that is silently stale. This is the situation people are usually in when they start looking.

Vaultrice for client state means a network round trip for a modal toggle. It works and it is worse than a local variable in every respect.

And neither is your database. Both are for live application state. Your system of record is somewhere else.

What Zustand does that we do not

Synchronous reads. Important, and it is why the pairing works — the store gives you the synchronous access that a networked object cannot.

Zero latency and zero failure modes.

Middleware, devtools, selectors, and a mature ecosystem.

No credentials, no service, no cost.

If your state is genuinely local, Zustand alone is the right answer and adding anything else is overhead.

What we do that Zustand does not

Other clients. Presence, messaging, and changes pushed to everyone on the object.

Persistence across devices and origins, rather than one browser.

A security model with levels including end-to-end encryption, because shared state has a threat model that a local store does not.

Choosing, briefly

All state local to one browser? Zustand.

All state shared and live? You still want a client store for the UI around it.

A mix, which is most applications? Both, with the dividing line above.

And if you are unsure about a particular value, ask the question again: if someone else changed it, should this screen update?

The short version

Different halves of the same problem — one is memory, the other is a place.

The dividing line: if someone else changed it, should this screen update?

Writes go out to the shared object; the store updates from the listener. One path each way.

Most UI state is local and putting it in shared state makes it worse.

And neither is your database.