React Native state management expert - Redux Toolkit, Zustand, TanStack Query, AsyncStorage, MMKV
Implements production-grade state management using Redux Toolkit, Zustand, TanStack Query, and data persistence solutions.
/plugin marketplace add pluginagentmarketplace/custom-plugin-react-native/plugin install react-native-assistant@pluginagentmarketplace-react-nativesonnetProduction-grade specialist for state management solutions, data persistence, caching strategies, and reactive data patterns in React Native.
01-react-native-fundamentals02-react-native-navigation04-react-native-native05-react-native-animation06-react-native-testing@reduxjs/toolkit - Modern Redux
react-redux - React bindings
redux-persist - State persistence
RTK Query - Data fetching & caching
createSlice - Reducer + actions
createAsyncThunk - Async operations
createEntityAdapter - Normalized state
zustand - Lightweight state
zustand/middleware - Persist, devtools, immer
zustand/shallow - Shallow comparison
@tanstack/react-query - Server state management
useQuery - Data fetching
useMutation - Data mutations
useInfiniteQuery - Pagination
queryClient - Cache management
@react-native-async-storage/async-storage - Key-value storage
react-native-mmkv - High performance storage
expo-secure-store - Encrypted storage
WatermelonDB - SQLite ORM
interface StateRequest {
type: 'setup' | 'pattern' | 'migration' | 'optimization' | 'debug';
solution: 'redux' | 'zustand' | 'tanstack-query' | 'context' | 'combined';
features?: ('persist' | 'offline' | 'typescript' | 'devtools')[];
dataType?: 'client' | 'server' | 'hybrid';
}
interface StateResponse {
architecture: ArchitectureGuide;
code: CodeExample[];
typeDefinitions?: string;
migrations?: MigrationPath[];
bestPractices: string[];
}
// store/index.ts - Store configuration
import { configureStore, combineReducers } from '@reduxjs/toolkit';
import {
persistStore,
persistReducer,
FLUSH,
REHYDRATE,
PAUSE,
PERSIST,
PURGE,
REGISTER,
} from 'redux-persist';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { setupListeners } from '@reduxjs/toolkit/query';
import { authSlice } from './slices/authSlice';
import { userSlice } from './slices/userSlice';
import { settingsSlice } from './slices/settingsSlice';
import { api } from './api';
const rootReducer = combineReducers({
auth: authSlice.reducer,
user: userSlice.reducer,
settings: settingsSlice.reducer,
[api.reducerPath]: api.reducer,
});
const persistConfig = {
key: 'root',
version: 1,
storage: AsyncStorage,
whitelist: ['auth', 'settings'], // Only persist these
blacklist: [api.reducerPath], // Never persist API cache
};
const persistedReducer = persistReducer(persistConfig, rootReducer);
export const store = configureStore({
reducer: persistedReducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: {
ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER],
},
}).concat(api.middleware),
devTools: __DEV__,
});
export const persistor = persistStore(store);
setupListeners(store.dispatch);
// Types
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
// store/slices/authSlice.ts
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
import { authApi } from '../../services/authApi';
interface User {
id: string;
email: string;
name: string;
avatar?: string;
}
interface AuthState {
user: User | null;
token: string | null;
refreshToken: string | null;
isAuthenticated: boolean;
isLoading: boolean;
error: string | null;
}
const initialState: AuthState = {
user: null,
token: null,
refreshToken: null,
isAuthenticated: false,
isLoading: false,
error: null,
};
// Async thunks
export const login = createAsyncThunk(
'auth/login',
async (
credentials: { email: string; password: string },
{ rejectWithValue }
) => {
try {
const response = await authApi.login(credentials);
return response.data;
} catch (error: any) {
return rejectWithValue(error.response?.data?.message || 'Login failed');
}
}
);
export const refreshAccessToken = createAsyncThunk(
'auth/refresh',
async (_, { getState, rejectWithValue }) => {
const state = getState() as { auth: AuthState };
const refreshToken = state.auth.refreshToken;
if (!refreshToken) {
return rejectWithValue('No refresh token');
}
try {
const response = await authApi.refresh(refreshToken);
return response.data;
} catch (error) {
return rejectWithValue('Token refresh failed');
}
}
);
export const authSlice = createSlice({
name: 'auth',
initialState,
reducers: {
logout: (state) => {
state.user = null;
state.token = null;
state.refreshToken = null;
state.isAuthenticated = false;
state.error = null;
},
clearError: (state) => {
state.error = null;
},
updateUser: (state, action: PayloadAction<Partial<User>>) => {
if (state.user) {
state.user = { ...state.user, ...action.payload };
}
},
},
extraReducers: (builder) => {
builder
// Login
.addCase(login.pending, (state) => {
state.isLoading = true;
state.error = null;
})
.addCase(login.fulfilled, (state, action) => {
state.isLoading = false;
state.user = action.payload.user;
state.token = action.payload.token;
state.refreshToken = action.payload.refreshToken;
state.isAuthenticated = true;
})
.addCase(login.rejected, (state, action) => {
state.isLoading = false;
state.error = action.payload as string;
})
// Refresh token
.addCase(refreshAccessToken.fulfilled, (state, action) => {
state.token = action.payload.token;
})
.addCase(refreshAccessToken.rejected, (state) => {
// Force logout on refresh failure
state.user = null;
state.token = null;
state.refreshToken = null;
state.isAuthenticated = false;
});
},
});
export const { logout, clearError, updateUser } = authSlice.actions;
// store/api/index.ts
import { createApi, fetchBaseQuery, retry } from '@reduxjs/toolkit/query/react';
import type { RootState } from '../index';
const baseQuery = fetchBaseQuery({
baseUrl: 'https://api.example.com/v1',
prepareHeaders: (headers, { getState }) => {
const token = (getState() as RootState).auth.token;
if (token) {
headers.set('Authorization', `Bearer ${token}`);
}
return headers;
},
});
const baseQueryWithRetry = retry(baseQuery, { maxRetries: 3 });
export const api = createApi({
reducerPath: 'api',
baseQuery: baseQueryWithRetry,
tagTypes: ['User', 'Posts', 'Comments'],
endpoints: (builder) => ({
// User endpoints
getUser: builder.query<User, string>({
query: (id) => `/users/${id}`,
providesTags: (result, error, id) => [{ type: 'User', id }],
}),
updateUser: builder.mutation<User, { id: string; data: Partial<User> }>({
query: ({ id, data }) => ({
url: `/users/${id}`,
method: 'PATCH',
body: data,
}),
invalidatesTags: (result, error, { id }) => [{ type: 'User', id }],
}),
// Posts with pagination
getPosts: builder.query<
{ posts: Post[]; total: number; page: number },
{ page?: number; limit?: number }
>({
query: ({ page = 1, limit = 20 }) =>
`/posts?page=${page}&limit=${limit}`,
providesTags: (result) =>
result
? [
...result.posts.map(({ id }) => ({ type: 'Posts' as const, id })),
{ type: 'Posts', id: 'LIST' },
]
: [{ type: 'Posts', id: 'LIST' }],
}),
createPost: builder.mutation<Post, CreatePostDto>({
query: (data) => ({
url: '/posts',
method: 'POST',
body: data,
}),
invalidatesTags: [{ type: 'Posts', id: 'LIST' }],
}),
}),
});
export const {
useGetUserQuery,
useUpdateUserMutation,
useGetPostsQuery,
useCreatePostMutation,
} = api;
// stores/useAppStore.ts
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
import { immer } from 'zustand/middleware/immer';
import AsyncStorage from '@react-native-async-storage/async-storage';
interface AppState {
// Theme
theme: 'light' | 'dark' | 'system';
setTheme: (theme: 'light' | 'dark' | 'system') => void;
// Notifications
notifications: Notification[];
addNotification: (notification: Notification) => void;
markAsRead: (id: string) => void;
clearNotifications: () => void;
unreadCount: () => number;
// User preferences
preferences: {
language: string;
pushEnabled: boolean;
biometricEnabled: boolean;
};
updatePreferences: (prefs: Partial<AppState['preferences']>) => void;
// Reset
reset: () => void;
}
const initialState = {
theme: 'system' as const,
notifications: [],
preferences: {
language: 'en',
pushEnabled: true,
biometricEnabled: false,
},
};
export const useAppStore = create<AppState>()(
persist(
immer((set, get) => ({
...initialState,
setTheme: (theme) =>
set((state) => {
state.theme = theme;
}),
addNotification: (notification) =>
set((state) => {
state.notifications.unshift(notification);
}),
markAsRead: (id) =>
set((state) => {
const notification = state.notifications.find((n) => n.id === id);
if (notification) {
notification.read = true;
}
}),
clearNotifications: () =>
set((state) => {
state.notifications = [];
}),
unreadCount: () => {
return get().notifications.filter((n) => !n.read).length;
},
updatePreferences: (prefs) =>
set((state) => {
Object.assign(state.preferences, prefs);
}),
reset: () => set(initialState),
})),
{
name: 'app-storage',
storage: createJSONStorage(() => AsyncStorage),
partialize: (state) => ({
theme: state.theme,
preferences: state.preferences,
// Don't persist notifications
}),
}
)
);
// Selector hooks for performance
export const useTheme = () => useAppStore((state) => state.theme);
export const usePreferences = () => useAppStore((state) => state.preferences);
export const useUnreadCount = () => useAppStore((state) => state.unreadCount());
// providers/QueryProvider.tsx
import React from 'react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { createAsyncStoragePersister } from '@tanstack/query-async-storage-persister';
import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client';
import AsyncStorage from '@react-native-async-storage/async-storage';
import NetInfo from '@react-native-community/netinfo';
import { onlineManager, focusManager } from '@tanstack/react-query';
import { AppState, Platform } from 'react-native';
// Online status management
onlineManager.setEventListener((setOnline) => {
return NetInfo.addEventListener((state) => {
setOnline(!!state.isConnected);
});
});
// Focus management for React Native
focusManager.setEventListener((handleFocus) => {
const subscription = AppState.addEventListener('change', (state) => {
handleFocus(state === 'active');
});
return () => subscription.remove();
});
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5, // 5 minutes
gcTime: 1000 * 60 * 60 * 24, // 24 hours (formerly cacheTime)
retry: 3,
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
networkMode: 'offlineFirst',
},
mutations: {
retry: 2,
networkMode: 'offlineFirst',
},
},
});
const asyncStoragePersister = createAsyncStoragePersister({
storage: AsyncStorage,
key: 'REACT_QUERY_CACHE',
});
export function QueryProvider({ children }: { children: React.ReactNode }) {
return (
<PersistQueryClientProvider
client={queryClient}
persistOptions={{
persister: asyncStoragePersister,
maxAge: 1000 * 60 * 60 * 24 * 7, // 7 days
buster: 'v1', // Increment to invalidate cache
}}
>
{children}
</PersistQueryClientProvider>
);
}
// hooks/useProducts.ts
import {
useQuery,
useMutation,
useQueryClient,
useInfiniteQuery,
} from '@tanstack/react-query';
import { productsApi } from '../services/productsApi';
// Keys factory for type safety and consistency
export const productKeys = {
all: ['products'] as const,
lists: () => [...productKeys.all, 'list'] as const,
list: (filters: ProductFilters) => [...productKeys.lists(), filters] as const,
details: () => [...productKeys.all, 'detail'] as const,
detail: (id: string) => [...productKeys.details(), id] as const,
};
// Infinite query for paginated list
export function useProductsInfinite(filters: ProductFilters) {
return useInfiniteQuery({
queryKey: productKeys.list(filters),
queryFn: ({ pageParam = 1 }) =>
productsApi.getProducts({ ...filters, page: pageParam }),
getNextPageParam: (lastPage) =>
lastPage.page < lastPage.totalPages ? lastPage.page + 1 : undefined,
initialPageParam: 1,
});
}
// Single product query
export function useProduct(id: string) {
return useQuery({
queryKey: productKeys.detail(id),
queryFn: () => productsApi.getProduct(id),
enabled: !!id,
});
}
// Create product mutation with optimistic update
export function useCreateProduct() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: productsApi.createProduct,
onMutate: async (newProduct) => {
// Cancel outgoing refetches
await queryClient.cancelQueries({ queryKey: productKeys.lists() });
// Snapshot previous value
const previousProducts = queryClient.getQueryData(productKeys.lists());
// Optimistically update
queryClient.setQueryData(productKeys.lists(), (old: any) => ({
...old,
products: [{ ...newProduct, id: 'temp-id' }, ...old.products],
}));
return { previousProducts };
},
onError: (err, newProduct, context) => {
// Rollback on error
queryClient.setQueryData(productKeys.lists(), context?.previousProducts);
},
onSettled: () => {
// Refetch after mutation
queryClient.invalidateQueries({ queryKey: productKeys.lists() });
},
});
}
// storage/mmkv.ts
import { MMKV } from 'react-native-mmkv';
import { StateStorage } from 'zustand/middleware';
// Create MMKV instance
export const storage = new MMKV({
id: 'app-storage',
encryptionKey: 'your-encryption-key', // Optional encryption
});
// Zustand storage adapter
export const zustandMMKVStorage: StateStorage = {
setItem: (name, value) => {
storage.set(name, value);
},
getItem: (name) => {
return storage.getString(name) ?? null;
},
removeItem: (name) => {
storage.delete(name);
},
};
// Typed storage helpers
export const appStorage = {
// String
getString: (key: string) => storage.getString(key),
setString: (key: string, value: string) => storage.set(key, value),
// Number
getNumber: (key: string) => storage.getNumber(key),
setNumber: (key: string, value: number) => storage.set(key, value),
// Boolean
getBoolean: (key: string) => storage.getBoolean(key),
setBoolean: (key: string, value: boolean) => storage.set(key, value),
// Object (JSON)
getObject: <T>(key: string): T | null => {
const value = storage.getString(key);
return value ? JSON.parse(value) : null;
},
setObject: <T>(key: string, value: T) => {
storage.set(key, JSON.stringify(value));
},
// Delete
delete: (key: string) => storage.delete(key),
// Clear all
clearAll: () => storage.clearAll(),
// Get all keys
getAllKeys: () => storage.getAllKeys(),
};
// Error handling in RTK Query
const api = createApi({
baseQuery: async (args, api, extraOptions) => {
const result = await baseQuery(args, api, extraOptions);
if (result.error) {
// Handle specific error codes
if (result.error.status === 401) {
api.dispatch(logout());
}
if (result.error.status === 503) {
// Show maintenance mode
api.dispatch(setMaintenanceMode(true));
}
}
return result;
},
});
const useStore = create<State>((set, get) => ({
error: null,
fetchData: async () => {
try {
set({ isLoading: true, error: null });
const data = await api.getData();
set({ data, isLoading: false });
} catch (error) {
set({
error: error instanceof Error ? error.message : 'Unknown error',
isLoading: false
});
}
},
}));
| Issue | Cause | Solution |
|---|---|---|
| "Non-serializable value" | Functions in state | Use middleware config |
| State not persisting | Wrong storage config | Check whitelist/blacklist |
| Stale data after mutation | Missing invalidation | Add proper tags |
| Memory leak warnings | Uncancelled queries | Use AbortController |
| Hydration mismatch | Async state restore | Use loading states |
// 1. Enable Redux DevTools
import { composeWithDevTools } from 'redux-devtools-extension';
// 2. Log Zustand state changes
const useStore = create(
devtools(
persist(/* ... */),
{ name: 'MyStore' }
)
);
// 3. React Query DevTools (for debugging)
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
// 4. Debug persistence
AsyncStorage.getAllKeys().then(keys => {
keys.forEach(key => {
AsyncStorage.getItem(key).then(value => {
console.log(key, value);
});
});
});
Task(subagent_type="react-native:03-react-native-state")
Bonded Skill: react-native-state
Use this agent to verify that a Python Agent SDK application is properly configured, follows SDK best practices and documentation recommendations, and is ready for deployment or testing. This agent should be invoked after a Python Agent SDK app has been created or modified.
Use this agent to verify that a TypeScript Agent SDK application is properly configured, follows SDK best practices and documentation recommendations, and is ready for deployment or testing. This agent should be invoked after a TypeScript Agent SDK app has been created or modified.