Skip to content
cccskills
FreePublished on GitHub

rn-state-and-storage

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).

The English summary is being prepared. Showing the original description.

Run score Not tested

See how to install

Files included (1)

  • SKILL.md9.9 KB

SKILL.md (original)

Review the instructions the agent will receive before you install.

State Management and Storage in RN

State management is largely identical to web React. Storage is the genuinely RN-specific concern.

State management — same as react-plugin

NeedTool
Local component stateuseState, useReducer
Shared between siblingsLift to common parent OR Context
App-wide UI state (theme, modals, sidebars)Context, Zustand, or Jotai
Server data with cachingTanStack Query, SWR
Complex domain stateRedux Toolkit (or Zustand for simpler cases)
Form statereact-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 in RN

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; } });

Zustand with persistence

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.

TanStack Query in RN

Works identically to web, but consider:

  • Network detection: @react-native-community/netinfo to gate queries when offline.
  • Persistence: @tanstack/react-query-persist-client + AsyncStorage/MMKV for offline-first apps.
  • Refetch on app foreground: TanStack Query has 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();
}, []);

Storage choices

The RN-specific decision. Each has a place.

AsyncStorage (@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:

  • User preferences (theme, locale, sort orders).
  • Onboarding flags ("user has seen welcome").
  • Cached non-sensitive responses.

NEVER:

  • Auth tokens.
  • PII / health / financial data.
  • Anything regulated (GDPR-sensitive).

MMKV (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:

  • User preferences (faster than AsyncStorage).
  • Frequently-read state (synchronous).
  • Encrypted-at-rest data (when paired with key from SecureStore).

Caveat: requires JSI / native build — works in dev-client, EAS Build, bare. NOT in Expo Go (managed without dev client).

SecureStore (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:

  • Auth tokens (JWT, refresh tokens, OAuth).
  • API keys that must persist across app launches (rare — prefer not persisting).
  • Biometric-protected secrets.

Limits:

  • ~2KB per value on iOS (Keychain limit).
  • Async API only.
  • Expo SDK package — works in managed and bare via Expo modules.

Keychain (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.

Decision matrix

DataStorage
Auth tokens (JWT, refresh)SecureStore / Keychain
OAuth tokensSecureStore / Keychain
User credentials (rare — prefer not storing)Keychain with biometric
Theme preferenceMMKV or AsyncStorage
LocaleMMKV or AsyncStorage
Onboarding seen flagMMKV 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 queriesexpo-sqlite, WatermelonDB, op-sqlite

Hydration on app start

Read storage, populate state, hide splash screen — in that order.

Expo + expo-splash-screen

// 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 hydration

Zustand persist middleware exposes hasHydrated:

const { isHydrated } = useUserStore.persist;
if (!isHydrated()) return <SplashScreen />;

Or subscribe via useStore selector:

const hasHydrated = useUserStore((s) => s._hasHydrated);

Bare RN

No Expo splash. Use react-native-bootsplash or implement custom splash UI.

Sensitive data handling rules

  1. JWT and refresh tokens: SecureStore / Keychain only. Never AsyncStorage.
  2. Don't log secrets: redact tokens before any console.log / analytics call.
  3. Clear on logout: explicitly delete all sensitive keys.
  4. Don't sync sensitive data via Settings.app / Cloud Backup: configure SecureStore with keychainService and access group flags.
  5. Encrypt large sensitive blobs: MMKV with encryption key from SecureStore (because SecureStore has size limits).

Anti-patterns

  • ❌ Storing JWT in AsyncStorage — readable by malicious apps with root/jailbreak access.
  • ❌ Persisting entire Redux store including server data — bloated AsyncStorage, slow restore.
  • ❌ Calling AsyncStorage in loops — batch with multiGet / multiSet.
  • ❌ Forgetting to await AsyncStorage operations — silent races.
  • ❌ Module-level mutable state — breaks Fast Refresh and SSR (RN Web).
  • ❌ Logging secrets via console.log for "debugging".
  • ❌ Mixing storage libs without a clear rationale (e.g., MMKV for some, AsyncStorage for others on the same app).
  • ❌ Showing UI before hydration completes — flicker or wrong-state render.
  • ❌ Storing user-modifiable data in Constants.expoConfig.extra — that's read-only build-time config, not runtime state.

Reviews

No reviews yet. Tell others what you thought.

Comments

No login needed. Share how it worked for you, ask a question, or note where it did not run.

No comments yet. Be the first to write one.

Comments are published right away. Do not include personal information such as email addresses or phone numbers. Up to 2 URLs.

0 / 1,000 characters

This form is protected by reCAPTCHA, and the Google Privacy Policy and Terms of Service apply.

More skills from this repository

Angular 18-21 project structure, standalone components vs NgModule, control flow (@if/@for/@switch + *ngIf/*ngFor legacy), decorators, dependency injection (inject() function), lifecycle hooks, pipes, Angular Universal SSR pointer. Use this skill to: - Detect project style (standalone vs NgModule) and apply matching patterns. - Pick correct decorators and DI approach. - Use modern control flow (@if/@for/@switch) in Angular 17+ projects. - Apply `inject()` function over constructor injection where appropriate. - Wire bootstrap correctly (bootstrapApplication for standalone, AppModule for legacy). Do NOT use this skill for: - State management (see angular-state-and-rx). - Routing (see angular-routing). - Forms (see angular-forms). - Testing (see angular-testing).

The English summary is being prepared. Showing the original description.

AratKruglik/claude-sdlc48Updated Oct 17, 2026

Angular forms: Reactive Forms (preferred — typed FormGroup/FormControl since Angular 14, FormBuilder, custom + async validators, FormArray, multi-step) and Template-driven (`[(ngModel)]` + FormsModule). Validation strategies, server error mapping, accessibility. Use this skill to: - Build Reactive Forms with typed FormGroup/FormControl. - Use FormBuilder для concise syntax. - Implement custom synchronous and async validators. - Wire FormArray for dynamic field lists. - Map server errors back to form fields. - Pick Reactive vs Template-driven (prefer Reactive). Do NOT use this skill for: - General conventions (see angular-conventions). - State management beyond forms (see angular-state-and-rx). - Routing (see angular-routing). - Testing forms (see angular-testing).

The English summary is being prepared. Showing the original description.

AratKruglik/claude-sdlc48Updated Oct 17, 2026

Angular Router (built-in `@angular/router`) — route configuration for standalone and NgModule projects, functional guards (Angular 14.1+), lazy loading, route resolvers, typed params via signals/observables, programmatic navigation, route data and meta. Use this skill to: - Configure routes (standalone-style or NgModule-style). - Use functional guards (canActivate as function, preferred over class-based in 17+). - Lazy-load components or feature modules. - Implement auth guards via route meta + functional guards. - Read params/queries via `inject(ActivatedRoute)` + signals or RxJS. Do NOT use this skill for: - General conventions (see angular-conventions). - State management (see angular-state-and-rx). - Forms (see angular-forms). - Testing routes (see angular-testing).

The English summary is being prepared. Showing the original description.

AratKruglik/claude-sdlc48Updated Oct 17, 2026

State management for Angular 18-21: signals (signal/computed/effect), services-as-state, NgRx Store + Effects + Selectors, NgRx Component Store, NgRx Signals (newer signal-based store). RxJS essentials — operators, async pipe, takeUntilDestroyed, signal/observable interop. Use this skill to: - Pick the right state tool (signals / services / NgRx variant / vue-query equivalent). - Use signals correctly (signal/computed/effect — when each). - Build a Pinia-style service-as-state singleton. - Set up NgRx Store + Effects + Selectors. - Use RxJS without leaking subscriptions (async pipe, takeUntilDestroyed, Subject patterns). - Bridge signals ↔ observables via toSignal / toObservable. Do NOT use this skill for: - General Angular conventions (see angular-conventions). - Routing state (see angular-routing). - Form state (see angular-forms). - Testing state (see angular-testing).

The English summary is being prepared. Showing the original description.

AratKruglik/claude-sdlc48Updated Oct 17, 2026

Testing Angular 18-21: TestBed, component harnesses (@angular/cdk/testing), Karma+Jasmine (default historical) vs Jest (jest-preset-angular, modern), Angular Testing Library (RTL-style). HttpClient mocking via HttpTestingController. NgRx Effects testing. Cypress / Playwright e2e. Use this skill to: - Detect runner (Karma+Jasmine vs Jest) and configure correctly. - Write component tests with TestBed. - Use component harnesses for Material / custom UI components. - Mock HttpClient via provideHttpClientTesting + HttpTestingController. - Test signal-based inputs with componentRef.setInput(). - Test NgRx Effects with provideMockActions. Do NOT use this skill for: - General Angular conventions (see angular-conventions). - Routing patterns broadly (see angular-routing — covers testing routes briefly). - Form patterns broadly (see angular-forms).

The English summary is being prepared. Showing the original description.

AratKruglik/claude-sdlc48Updated Oct 17, 2026

Shared conventions for every SDLC development-phase architect agent: hard rules, code quality bar, workflow steps (superpowers invocation, spec reading, codebase exploration, verification), and the report/compact-summary contract. Architects load this skill first, then apply their stack-specific instructions on top.

The English summary is being prepared. Showing the original description.

AratKruglik/claude-sdlc48Updated Oct 17, 2026

See all skills by AratKruglik

Report a problem with this skill