From react-native-plugin
State management and storage choice in React Native. State libs work mostly the same as web (Zustand, Jotai, Redux Toolkit, TanStack Query, Context); storage choice is RN-specific — AsyncStorage, MMKV, SecureStore, Keychain. Hydration patterns, splash screen handling, secrets discipline. Use this skill to: - Pick state management lib (matches web React intuition). - Pick storage by sensitivity (AsyncStorage / MMKV / SecureStore / Keychain). - Hydrate state on app start without UI flicker. - Persist Zustand store across app restarts. - Avoid storing secrets in plaintext storage. Do NOT use this skill for: - General project conventions (see rn-conventions). - Platform-specific code (see rn-platform-specific). - Navigation (see rn-navigation). - Testing state/storage (see rn-testing).
How this skill is triggered — by the user, by Claude, or both
Slash command
/react-native-plugin:rn-state-and-storageThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
State management is largely identical to web React. Storage is the genuinely RN-specific concern.
State management is largely identical to web React. Storage is the genuinely RN-specific concern.
| Need | Tool |
|---|---|
| Local component state | useState, useReducer |
| Shared between siblings | Lift to common parent OR Context |
| App-wide UI state (theme, modals, sidebars) | Context, Zustand, or Jotai |
| Server data with caching | TanStack Query, SWR |
| Complex domain state | Redux Toolkit (or Zustand for simpler cases) |
| Form state | react-hook-form |
Same decision tree as web — see react-plugin:react-state-management skill for deep dive on each tool. RN-specific notes below.
Context survives Fast Refresh ONLY if values are not held in module-level closure. Store provider state in useState / useReducer inside the provider component, not in a top-level let.
// ✅ Survives Fast Refresh
export function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState<Theme>('light');
return <ThemeContext.Provider value={{ theme, setTheme }}>{children}</ThemeContext.Provider>;
}
// ❌ Lost on Fast Refresh
let theme: Theme = 'light';
export const ThemeContext = createContext({ theme, setTheme: (t: Theme) => { theme = t; } });
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
import AsyncStorage from '@react-native-async-storage/async-storage';
export const useUserStore = create(
persist(
(set) => ({ user: null, setUser: (user: User | null) => set({ user }) }),
{
name: 'user-storage',
storage: createJSONStorage(() => AsyncStorage),
}
)
);
For sensitive data, swap AsyncStorage for SecureStore wrapper:
import * as SecureStore from 'expo-secure-store';
const secureStorage = {
getItem: (key: string) => SecureStore.getItemAsync(key),
setItem: (key: string, value: string) => SecureStore.setItemAsync(key, value),
removeItem: (key: string) => SecureStore.deleteItemAsync(key),
};
storage: createJSONStorage(() => secureStorage),
Note: SecureStore values are limited to ~2KB on iOS. For larger sensitive data, encrypt with expo-crypto and store ciphertext in MMKV.
Works identically to web, but consider:
@react-native-community/netinfo to gate queries when offline.@tanstack/react-query-persist-client + AsyncStorage/MMKV for offline-first apps.refetchOnWindowFocus (web); for RN, manually trigger via AppState listener.import { AppState } from 'react-native';
import { focusManager } from '@tanstack/react-query';
useEffect(() => {
const sub = AppState.addEventListener('change', (state) => {
focusManager.setFocused(state === 'active');
});
return () => sub.remove();
}, []);
The RN-specific decision. Each has a place.
@react-native-async-storage/async-storage)Async key-value, plaintext on disk. Most common, but NOT secure.
import AsyncStorage from '@react-native-async-storage/async-storage';
await AsyncStorage.setItem('@theme', 'dark');
const theme = await AsyncStorage.getItem('@theme'); // string | null
await AsyncStorage.removeItem('@theme');
await AsyncStorage.multiGet(['@a', '@b']); // [[key, value], ...]
await AsyncStorage.multiSet([['@a', '1'], ['@b', '2']]);
Use for:
NEVER:
react-native-mmkv)Synchronous, fast (~30x faster than AsyncStorage), encrypted optional. Modern preferred for non-secrets.
import { MMKV } from 'react-native-mmkv';
const storage = new MMKV();
storage.set('theme', 'dark'); // sync, no await needed
const theme = storage.getString('theme'); // 'dark' or undefined
storage.delete('theme');
storage.contains('theme'); // boolean
storage.getAllKeys(); // string[]
Optional encryption:
const storage = new MMKV({
id: 'user-storage',
encryptionKey: 'a-secret-key-from-secure-store',
});
Use for:
Caveat: requires JSI / native build — works in dev-client, EAS Build, bare. NOT in Expo Go (managed without dev client).
expo-secure-store)Keychain (iOS) / EncryptedSharedPreferences (Android). For sensitive data.
import * as SecureStore from 'expo-secure-store';
await SecureStore.setItemAsync('refresh-token', token);
const token = await SecureStore.getItemAsync('refresh-token');
await SecureStore.deleteItemAsync('refresh-token');
Use for:
Limits:
react-native-keychain)Bare alternative to SecureStore. Same purpose, more configuration options (access groups, biometric prompts).
import * as Keychain from 'react-native-keychain';
await Keychain.setGenericPassword('username', 'password');
const credentials = await Keychain.getGenericPassword();
if (credentials) console.log(credentials.username, credentials.password);
await Keychain.resetGenericPassword();
For finer control:
await Keychain.setInternetCredentials('api.example.com', 'username', 'token', {
accessControl: Keychain.ACCESS_CONTROL.BIOMETRY_CURRENT_SET,
accessible: Keychain.ACCESSIBLE.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
});
Use in bare projects or when SecureStore's API is too limited.
| Data | Storage |
|---|---|
| Auth tokens (JWT, refresh) | SecureStore / Keychain |
| OAuth tokens | SecureStore / Keychain |
| User credentials (rare — prefer not storing) | Keychain with biometric |
| Theme preference | MMKV or AsyncStorage |
| Locale | MMKV or AsyncStorage |
| Onboarding seen flag | MMKV or AsyncStorage |
| Cached server data (offline-first) | TanStack Query persist + MMKV/AsyncStorage |
| Large blobs (images, files) | expo-file-system (managed) or react-native-fs (bare) |
| Database-like queries | expo-sqlite, WatermelonDB, op-sqlite |
Read storage, populate state, hide splash screen — in that order.
// app/_layout.tsx (Expo Router) or App.tsx
import * as SplashScreen from 'expo-splash-screen';
import { useEffect, useState } from 'react';
import { useUserStore } from '@/stores/userStore';
SplashScreen.preventAutoHideAsync();
export default function RootLayout() {
const [hydrated, setHydrated] = useState(false);
const restoreUser = useUserStore((s) => s.restoreUser);
useEffect(() => {
(async () => {
await restoreUser();
setHydrated(true);
})();
}, [restoreUser]);
useEffect(() => {
if (hydrated) SplashScreen.hideAsync();
}, [hydrated]);
if (!hydrated) return null;
return <App />;
}
Zustand persist middleware exposes hasHydrated:
const { isHydrated } = useUserStore.persist;
if (!isHydrated()) return <SplashScreen />;
Or subscribe via useStore selector:
const hasHydrated = useUserStore((s) => s._hasHydrated);
No Expo splash. Use react-native-bootsplash or implement custom splash UI.
console.log / analytics call.keychainService and access group flags.multiGet / multiSet.await AsyncStorage operations — silent races.console.log for "debugging".Constants.expoConfig.extra — that's read-only build-time config, not runtime state.npx claudepluginhub aratkruglik/claude-sdlc --plugin react-native-pluginGuides on persisting data in React Native/Expo apps using AsyncStorage, SecureStore, MMKV, and SQLite for different use cases.
Guides data fetching in React Native/Expo apps using Fetch API and TanStack Query, covering API calls, caching, mutations, authentication tokens, offline support, and request cancellation.
Guides React Native/Expo development with patterns for Expo Router navigation, state separation, data fetching (TanStack Query + Zod), styling, and native APIs.