In a React Native workout app, what belongs in a persisted store, and how do partialize or a redux-persist whitelist keep the rest out?
answer
- persist what the user would retype
- not loading flags or open modals
- no tokens in plain storage
- Zustand partialize returns the subset
- whitelist and blacklist are one level deep
basics
~20 sPersist only what the user would lose work or context without, such as the unfinished workout log and preferences, not transient flags, refetchable server data or secrets. Zustand's partialize and redux-persist's whitelist or blacklist choose the subset.
solid answer
~50 sA persisted store is written to plain on-device storage, AsyncStorage or MMKV, and read back on every cold start, so everything in it costs write time, read time and a migration later. In a workout app the half-finished log and the user's settings belong there; `isSaving`, an open picker, the current scroll position and data fetched from the server do not, and tokens belong in secure storage instead. With Zustand's `persist` middleware, `partialize: (s) => ({ log: s.log, settings: s.settings })` returns the subset to write. With redux-persist, the config's `whitelist` lists the top-level reducer keys to keep, or `blacklist` the ones to skip; both work only one level deep, so filtering deeper needs a nested `persistReducer`. Persisting too much also means a flag like `isSaving: true` survives a crash and the app starts stuck.
code
typescript · 27 linesimport AsyncStorage from '@react-native-async-storage/async-storage';
import { create } from 'zustand';
import { createJSONStorage, persist } from 'zustand/middleware';
type WorkoutSet = { exercise: string; reps: number; weightKg: number };
type WorkoutState = {
log: WorkoutSet[];
startedAt: number | null; // a number, not a Date: survives JSON
isSaving: boolean; // transient: never persisted
addSet: (s: WorkoutSet) => void;
};
export const useWorkout = create<WorkoutState>()(
persist(
(set) => ({
log: [],
startedAt: null,
isSaving: false,
addSet: (s) => set((st) => ({ log: [...st.log, s] })),
}),
{
name: 'workout',
storage: createJSONStorage(() => AsyncStorage),
partialize: (s) => ({ log: s.log, startedAt: s.startedAt }),
},
),
);go deeper
Recall the rule: persist what the user would lose work without, and leave out flags, server data and secrets.
Explain partialize and whitelist or blacklist, the one-level limit, and what JSON does to dates and maps.
Design the persisted shape deliberately as an allow-list, anticipating migration cost and the stuck-flag bug after process death.
Set team rules for what may be written to plain storage and when data outgrows a store and needs a database.
## What persistence costs on a phone A persisted store copies part of your in-memory state to **on-device storage**, such as AsyncStorage or MMKV, and restores it on the next cold start. Every field you include: - is **serialized and written** as the state changes, work that runs on the JavaScript thread; - is **read and parsed** at startup, before the app can show the restored screen; - becomes a **stored shape** that future releases must migrate; - sits in **plain, unencrypted** app storage. So the question "what do we persist?" is a design decision, not a default. ## What belongs, and what does not For an app that must restore a half-finished workout after it is killed: | State | Persist? | Why | |---|---|---| | unfinished workout log: exercises, sets, reps, start time | yes | the user loses work without it | | units, rest-timer length, theme | yes | preferences the user set once | | `isSaving`, `isLoading`, error banners | no | a crash would restore a stuck spinner | | which modal or picker is open | no | transient UI, meaningless after restart | | exercise catalogue fetched from the API | no | a server-state cache owns it and refetches | | auth tokens, API keys | no | secure storage, not a plain store | | derived totals such as volume per session | no | recompute from the log | A useful test: **would the user notice or lose work if this were gone after a restart?** If not, leave it out. ## Selecting the subset **Zustand `persist`.** The `partialize` option receives the full state and returns the object to store. Anything not returned, including action functions, is never written: - return only the keys you want, such as the log and settings; - do not spread the whole state and delete a few keys, since every new field then becomes persisted by default; - keep it a pure function of state, because it runs on every write. **redux-persist.** The `persistReducer` config takes `whitelist` (keys to keep) or `blacklist` (keys to skip). Two details matter: 1. both lists match **top-level keys of the reducer you wrap**, one level deep; 2. to drop a field inside a slice, such as `workout.isSaving`, wrap that slice in its own `persistReducer` with its own `blacklist`, the nested-persist pattern the redux-persist README describes. Prefer an allow-list (`partialize` returning named keys, or `whitelist`) over a block-list: new fields then stay out until someone decides otherwise. ## Serialization limits Stores persist through JSON by default, so what you persist must survive `JSON.stringify` and `JSON.parse`: - a `Date` comes back as a string, so store timestamps as numbers; - `Map`, `Set` and class instances lose their type; - functions are dropped. Keeping the persisted shape plain also keeps migrations simple. ## Why the flag case matters A persisted `isSaving: true` is the classic React Native bug here. The user taps Save, the OS kills the app in the background, and on the next launch the store restores a save that will never finish: the Save button stays disabled forever. Excluding transient flags from persistence prevents the whole class of bug. ## Where the persisted data should not go Credentials and tokens belong in the platform's secure storage, not in a store written to AsyncStorage or MMKV without encryption. Relational data that grows without bound, such as years of workout history, fits a database better than one serialized blob that is rewritten on every change.
- Why is a block-list riskier than an allow-list for a persisted React Native store?With a block-list, every field someone adds later is persisted unless they remember to exclude it, so transient flags and large caches drift into storage and into future migrations. An allow-list, such as `partialize` returning named keys or a redux-persist `whitelist`, keeps new fields out until someone decides they are worth restoring.
- The app restores with the Save button permanently disabled. What happened?A transient flag such as `isSaving: true` was persisted. The OS killed the app while a save was in flight, and on the next launch the store restored the flag, but no request is running to reset it. Exclude such flags from persistence, or reset them explicitly after hydration.
saying these in an interview costs you the question
- Persisting the whole store is fine because storage is cheap
- Auth tokens can live in the persisted store beside the cart
- redux-persist's blacklist can exclude a field nested inside a slice
- A Date in persisted state comes back as a Date after restart
- Server responses should be persisted in the client store for offline use