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 zustandThat'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,mmkvorasync-storage);persistwrites 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-modulesIn a finalized tree setup is a stub, so you undo it by hand. Here is everything this module added:
- 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. - Uninstall the dependencies:
bun remove zustand. - Check it:
bun run typecheckandbun run lintpoint at anything that still imports the removed files;bun run gen:graphrefreshesdocs/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 zustandModule id: state/zustand (the default for this category).
Dependencies
| Package | Version | Kind |
|---|---|---|
zustand | ^5.0.15 | dependency |
Compatibility
- Requires
storage=kv-store,mmkv,async-storage
Files
5 files copied to the project root
src/app/examples/store.tsxsrc/screens/examples/store-example-screen.tsxsrc/stores/__tests__/example-store.test.tssrc/stores/example-store.tssrc/stores/zustand-storage.ts