Jotai
Set up Jotai for state in an Expo app with ReadyNative: atomic state, persisted on the storage adapter · Expo Go OK
Expo Go: yes
Runs in Expo Go; no dev build needed for this module.
Jotai gives you atoms: tiny pieces of state you compose, with persistence through atomWithAppStorage on your storage adapter. I'd pick it over zustand when your state is naturally lots of small independent values and you want derived state for free, and over none as soon as two screens share something. The built-in theme and onboarding stores stay on createPersistedStore in core either way.
Setup
bun run setup --state jotaiThat's it - setup wires the storage bridge for you. It adds src/stores/jotai-storage.ts (atomWithAppStorage(key, initial), plus createJotaiStorage<Value>() and hasSyncStorage for custom atoms), and a test, plus the example src/stores/example-store.ts and the /examples/store route and screen with --with-examples. There's no provider: atoms live in Jotai's default store. Add <Provider> from jotai in src/providers.tsx only if you need isolated stores - tests can use createStore() instead.
Usage
A persisted atom is one line:
export const settingsAtom = atomWithAppStorage("readynative:settings", { sound: true });Its type is Value | Promise<Value> because the web path loads asynchronously. Read it with useAtomValue (which unwraps), or derive a sync view like this:
export const countAtom = unwrap(counterAtom, (prev) => prev ?? 0);
export const incrementAtom = atom(null, (get, set) => set(counterAtom, get(countAtom) + 1));Prefer write-only action atoms like that over set((prev) => …), so an updater never receives a promise. Outside React, use getDefaultStore().get(countAtom) and .set(incrementAtom).
Gotchas
- Expo Go works - it's pure JS.
- A storage module has to be selected (
kv-store,mmkvorasync-storage). - On web the first render shows the initial value until the async read resolves, and a write during that window overwrites the stored value (same as zustand
persiston web). - The module pins
jotai^2.x. Version 3.0.0 exists (2026-09-08) and thejotai/vanilla/utils/atomWithStoragetype imports may move in it. - Swapping stacks:
bun run setup --state none(or--state zustand) 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/jotai-storage.ts. - Uninstall the dependencies:
bun remove jotai. - 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/jotai/module.json - the same file bun run setup reads, so it is what actually lands in your repo.
Install
bun run setup --state jotaiModule id: state/jotai.
Dependencies
| Package | Version | Kind |
|---|---|---|
jotai | ^2.20.3 | 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/jotai-storage.ts