<< All versions
Skill v1.0.1
currentAutomated scan100/100freekmurze/dotfiles/react-state-management
1 files
──Details
PublishedMay 17, 2026 at 02:03 PM
Content Hashsha256:de147720d385f3b0...
Git SHA3974caaa4459
Bump Typepatch
──Files
Files (1 file, 11.5 KB)
SKILL.md11.5 KBactive
SKILL.md · 439 lines · 11.5 KB
version: "1.0.1" name: react-state-management description: Master modern React state management with Redux Toolkit, Zustand, Jotai, and React Query. Use when setting up global state, managing server state, or choosing between state management solutions.
React State Management
Comprehensive guide to modern React state management patterns, from local component state to global stores and server state synchronization.
When to Use This Skill
- Setting up global state management in a React app
- Choosing between Redux Toolkit, Zustand, or Jotai
- Managing server state with React Query or SWR
- Implementing optimistic updates
- Debugging state-related issues
- Migrating from legacy Redux to modern patterns
Core Concepts
1. State Categories
| Type | Description | Solutions | |
|---|---|---|---|
| Local State | Component-specific, UI state | useState, useReducer | |
| Global State | Shared across components | Redux Toolkit, Zustand, Jotai | |
| Server State | Remote data, caching | React Query, SWR, RTK Query | |
| URL State | Route parameters, search | React Router, nuqs | |
| Form State | Input values, validation | React Hook Form, Formik |
2. Selection Criteria
Small app, simple state → Zustand or JotaiLarge app, complex state → Redux ToolkitHeavy server interaction → React Query + light client stateAtomic/granular updates → Jotai
Quick Start
Zustand (Simplest)
typescript
// store/useStore.tsimport { create } from 'zustand'import { devtools, persist } from 'zustand/middleware'interface AppState {user: User | nulltheme: 'light' | 'dark'setUser: (user: User | null) => voidtoggleTheme: () => void}export const useStore = create<AppState>()(devtools(persist((set) => ({user: null,theme: 'light',setUser: (user) => set({ user }),toggleTheme: () => set((state) => ({theme: state.theme === 'light' ? 'dark' : 'light'})),}),{ name: 'app-storage' })))// Usage in componentfunction Header() {const { user, theme, toggleTheme } = useStore()return (<header className={theme}>{user?.name}<button onClick={toggleTheme}>Toggle Theme</button></header>)}
Patterns
Pattern 1: Redux Toolkit with TypeScript
typescript
// store/index.tsimport { configureStore } from "@reduxjs/toolkit";import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux";import userReducer from "./slices/userSlice";import cartReducer from "./slices/cartSlice";export const store = configureStore({reducer: {user: userReducer,cart: cartReducer,},middleware: (getDefaultMiddleware) =>getDefaultMiddleware({serializableCheck: {ignoredActions: ["persist/PERSIST"],},}),});export type RootState = ReturnType<typeof store.getState>;export type AppDispatch = typeof store.dispatch;// Typed hooksexport const useAppDispatch: () => AppDispatch = useDispatch;export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
typescript
// store/slices/userSlice.tsimport { createSlice, createAsyncThunk, PayloadAction } from "@reduxjs/toolkit";interface User {id: string;email: string;name: string;}interface UserState {current: User | null;status: "idle" | "loading" | "succeeded" | "failed";error: string | null;}const initialState: UserState = {current: null,status: "idle",error: null,};export const fetchUser = createAsyncThunk("user/fetchUser",async (userId: string, { rejectWithValue }) => {try {const response = await fetch(`/api/users/${userId}`);if (!response.ok) throw new Error("Failed to fetch user");return await response.json();} catch (error) {return rejectWithValue((error as Error).message);}},);const userSlice = createSlice({name: "user",initialState,reducers: {setUser: (state, action: PayloadAction<User>) => {state.current = action.payload;state.status = "succeeded";},clearUser: (state) => {state.current = null;state.status = "idle";},},extraReducers: (builder) => {builder.addCase(fetchUser.pending, (state) => {state.status = "loading";state.error = null;}).addCase(fetchUser.fulfilled, (state, action) => {state.status = "succeeded";state.current = action.payload;}).addCase(fetchUser.rejected, (state, action) => {state.status = "failed";state.error = action.payload as string;});},});export const { setUser, clearUser } = userSlice.actions;export default userSlice.reducer;
Pattern 2: Zustand with Slices (Scalable)
typescript
// store/slices/createUserSlice.tsimport { StateCreator } from "zustand";export interface UserSlice {user: User | null;isAuthenticated: boolean;login: (credentials: Credentials) => Promise<void>;logout: () => void;}export const createUserSlice: StateCreator<UserSlice & CartSlice, // Combined store type[],[],UserSlice> = (set, get) => ({user: null,isAuthenticated: false,login: async (credentials) => {const user = await authApi.login(credentials);set({ user, isAuthenticated: true });},logout: () => {set({ user: null, isAuthenticated: false });// Can access other slices// get().clearCart()},});// store/index.tsimport { create } from "zustand";import { createUserSlice, UserSlice } from "./slices/createUserSlice";import { createCartSlice, CartSlice } from "./slices/createCartSlice";type StoreState = UserSlice & CartSlice;export const useStore = create<StoreState>()((...args) => ({...createUserSlice(...args),...createCartSlice(...args),}));// Selective subscriptions (prevents unnecessary re-renders)export const useUser = () => useStore((state) => state.user);export const useCart = () => useStore((state) => state.cart);
Pattern 3: Jotai for Atomic State
typescript
// atoms/userAtoms.tsimport { atom } from 'jotai'import { atomWithStorage } from 'jotai/utils'// Basic atomexport const userAtom = atom<User | null>(null)// Derived atom (computed)export const isAuthenticatedAtom = atom((get) => get(userAtom) !== null)// Atom with localStorage persistenceexport const themeAtom = atomWithStorage<'light' | 'dark'>('theme', 'light')// Async atomexport const userProfileAtom = atom(async (get) => {const user = get(userAtom)if (!user) return nullconst response = await fetch(`/api/users/${user.id}/profile`)return response.json()})// Write-only atom (action)export const logoutAtom = atom(null, (get, set) => {set(userAtom, null)set(cartAtom, [])localStorage.removeItem('token')})// Usagefunction Profile() {const [user] = useAtom(userAtom)const [, logout] = useAtom(logoutAtom)const [profile] = useAtom(userProfileAtom) // Suspense-enabledreturn (<Suspense fallback={<Skeleton />}><ProfileContent profile={profile} onLogout={logout} /></Suspense>)}
Pattern 4: React Query for Server State
typescript
// hooks/useUsers.tsimport { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";// Query keys factoryexport const userKeys = {all: ["users"] as const,lists: () => [...userKeys.all, "list"] as const,list: (filters: UserFilters) => [...userKeys.lists(), filters] as const,details: () => [...userKeys.all, "detail"] as const,detail: (id: string) => [...userKeys.details(), id] as const,};// Fetch hookexport function useUsers(filters: UserFilters) {return useQuery({queryKey: userKeys.list(filters),queryFn: () => fetchUsers(filters),staleTime: 5 * 60 * 1000, // 5 minutesgcTime: 30 * 60 * 1000, // 30 minutes (formerly cacheTime)});}// Single user hookexport function useUser(id: string) {return useQuery({queryKey: userKeys.detail(id),queryFn: () => fetchUser(id),enabled: !!id, // Don't fetch if no id});}// Mutation with optimistic updateexport function useUpdateUser() {const queryClient = useQueryClient();return useMutation({mutationFn: updateUser,onMutate: async (newUser) => {// Cancel outgoing refetchesawait queryClient.cancelQueries({queryKey: userKeys.detail(newUser.id),});// Snapshot previous valueconst previousUser = queryClient.getQueryData(userKeys.detail(newUser.id),);// Optimistically updatequeryClient.setQueryData(userKeys.detail(newUser.id), newUser);return { previousUser };},onError: (err, newUser, context) => {// Rollback on errorqueryClient.setQueryData(userKeys.detail(newUser.id),context?.previousUser,);},onSettled: (data, error, variables) => {// Refetch after mutationqueryClient.invalidateQueries({queryKey: userKeys.detail(variables.id),});},});}
Pattern 5: Combining Client + Server State
typescript
// Zustand for client stateconst useUIStore = create<UIState>((set) => ({sidebarOpen: true,modal: null,toggleSidebar: () => set((s) => ({ sidebarOpen: !s.sidebarOpen })),openModal: (modal) => set({ modal }),closeModal: () => set({ modal: null }),}))// React Query for server statefunction Dashboard() {const { sidebarOpen, toggleSidebar } = useUIStore()const { data: users, isLoading } = useUsers({ active: true })const { data: stats } = useStats()if (isLoading) return <DashboardSkeleton />return (<div className={sidebarOpen ? 'with-sidebar' : ''}><Sidebar open={sidebarOpen} onToggle={toggleSidebar} /><main><StatsCards stats={stats} /><UserTable users={users} /></main></div>)}
Best Practices
Do's
- Colocate state - Keep state as close to where it's used as possible
- Use selectors - Prevent unnecessary re-renders with selective subscriptions
- Normalize data - Flatten nested structures for easier updates
- Type everything - Full TypeScript coverage prevents runtime errors
- Separate concerns - Server state (React Query) vs client state (Zustand)
Don'ts
- Don't over-globalize - Not everything needs to be in global state
- Don't duplicate server state - Let React Query manage it
- Don't mutate directly - Always use immutable updates
- Don't store derived data - Compute it instead
- Don't mix paradigms - Pick one primary solution per category
Migration Guides
From Legacy Redux to RTK
typescript
// Before (legacy Redux)const ADD_TODO = "ADD_TODO";const addTodo = (text) => ({ type: ADD_TODO, payload: text });function todosReducer(state = [], action) {switch (action.type) {case ADD_TODO:return [...state, { text: action.payload, completed: false }];default:return state;}}// After (Redux Toolkit)const todosSlice = createSlice({name: "todos",initialState: [],reducers: {addTodo: (state, action: PayloadAction<string>) => {// Immer allows "mutations"state.push({ text: action.payload, completed: false });},},});