ReadyNative

Zustand

Set up Zustand for state in an Expo app with ReadyNative: tiny stores, persisted on the storage adapter · Expo Go OK

Expo Go: yes

Runs in Expo Go; no dev build needed for this module.

Zustand gives you small global stores with a hook-shaped API and a persist middleware already pointed at your storage adapter. I'd pick it over jotai when you think in stores and selectors rather than in atoms, and over none as soon as two screens need to share state. It doesn't replace the built-in stores: theme mode and onboarding stay on createPersistedStore in core.

Setup

bun run setup --state zustand

That's it - setup wires the storage bridge for you. It adds src/stores/zustand-storage.ts (a StateStorage on the app storage adapter, sync on native and async on web), and a test, plus the example src/stores/example-store.ts and the /examples/store route and screen with --with-examples.

Usage

A persisted store looks like this (src/stores/example-store.ts with --with-examples):

import { create } from "zustand";
import { createJSONStorage, persist } from "zustand/middleware";
import { zustandStorage } from "./zustand-storage";

export const useCounterStore = create<CounterState>()(
  persist(
    (set) => ({
      count: 0,
      increment: () => set((s) => ({ count: s.count + 1 })),
      reset: () => set({ count: 0 }),
    }),
    {
      name: "readynative:counter",
      storage: createJSONStorage(() => zustandStorage),
      partialize: (s) => ({ count: s.count }),
    }
  )
);

Select a slice in components so you only re-render on what you read:

const count = useCounterStore((s) => s.count);

Outside React, reach for useCounterStore.getState().increment().

Gotchas

  • Expo Go works - it's pure JS.
  • A storage module has to be selected (kv-store, mmkv or async-storage); persist writes through whichever one you picked.
  • On web the adapter is async, so the first render shows the initial state until hydration finishes. Gate first paint on hydration if a wrong first frame would be visible.
  • Swapping stacks: bun run setup --state none (or --state jotai) removes the files and the dependency.

Remove it

While modules/ exists (a tree set up with --keep-modules), setup does all of it:

bun run setup --state none --yes --keep-modules

In a finalized tree setup is a stub, so you undo it by hand. Here is everything this module added:

  1. Delete the files that are still there (the demo screens are gone already unless you set up with --with-examples): src/app/examples/store.tsx, src/screens/examples/store-example-screen.tsx, src/stores/__tests__/example-store.test.ts, src/stores/example-store.ts, src/stores/zustand-storage.ts.
  2. Uninstall the dependencies: bun remove zustand.
  3. Check it: bun run typecheck and bun run lint point at anything that still imports the removed files; bun run gen:graph refreshes docs/ARCHITECTURE.md.

Reference

Everything below is generated from modules/state/zustand/module.json - the same file bun run setup reads, so it is what actually lands in your repo.

Install

bun run setup --state zustand

Module id: state/zustand (the default for this category).

Dependencies

PackageVersionKind
zustand^5.0.15dependency

Compatibility

Files

5 files copied to the project root
  • src/app/examples/store.tsx
  • src/screens/examples/store-example-screen.tsx
  • src/stores/__tests__/example-store.test.ts
  • src/stores/example-store.ts
  • src/stores/zustand-storage.ts

On this page

Get ReadyNative