Modern State Management Implementation
1. Redux Toolkit RTK Query Setup
| Feature | Syntax | Description | Use Case |
|---|---|---|---|
| configureStore | configureStore({ reducer: {} }) |
Simplifies store setup with good defaults, DevTools | Store initialization |
| createSlice | createSlice({ name, initialState, reducers }) |
Generates action creators and reducers automatically | State domain logic |
| createAsyncThunk | createAsyncThunk('type', async () => {}) |
Handles async logic with pending/fulfilled/rejected actions | API calls, side effects |
| createApi | createApi({ baseQuery, endpoints }) |
RTK Query API service with auto-generated hooks | Data fetching & caching |
| useSelector | const data = useSelector(state => state.data) |
Extracts state with automatic re-render on changes | Access store state |
| useDispatch | const dispatch = useDispatch() |
Returns dispatch function to trigger actions | Update state |
Example: Redux Toolkit with RTK Query complete setup
// store.ts
import { configureStore } from '@reduxjs/toolkit';
import { setupListeners } from '@reduxjs/toolkit/query';
import { api } from './api';
import counterReducer from './counterSlice';
export const store = configureStore({
reducer: {
counter: counterReducer,
[api.reducerPath]: api.reducer,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(api.middleware),
});
setupListeners(store.dispatch);
// counterSlice.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
interface CounterState {
value: number;
status: 'idle' | 'loading';
}
const counterSlice = createSlice({
name: 'counter',
initialState: { value: 0, status: 'idle' } as CounterState,
reducers: {
increment: (state) => {
state.value += 1;
},
decrement: (state) => {
state.value -= 1;
},
incrementByAmount: (state, action: PayloadAction<number>) => {
state.value += action.payload;
},
},
});
export const { increment, decrement, incrementByAmount } = counterSlice.actions;
export default counterSlice.reducer;
// api.ts - RTK Query
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
export const api = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
tagTypes: ['User', 'Post'],
endpoints: (builder) => ({
getUsers: builder.query<User[], void>({
query: () => '/users',
providesTags: ['User'],
}),
getUserById: builder.query<User, string>({
query: (id) => `/users/${id}`,
providesTags: (result, error, id) => [{ type: 'User', id }],
}),
createUser: builder.mutation<User, Partial<User>>({
query: (body) => ({
url: '/users',
method: 'POST',
body,
}),
invalidatesTags: ['User'],
}),
}),
});
export const { useGetUsersQuery, useGetUserByIdQuery, useCreateUserMutation } = api;
// Component usage
function UserList() {
const { data: users, isLoading, error } = useGetUsersQuery();
const [createUser] = useCreateUserMutation();
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error loading users</div>;
return (
<div>
{users?.map(user => <div key={user.id}>{user.name}</div>)}
<button onClick={() => createUser({ name: 'New User' })}>
Add User
</button>
</div>
);
}
2. Zustand Lightweight Store Creation
| Feature | Syntax | Description | Use Case |
|---|---|---|---|
| create | create((set) => ({ ...state })) |
Creates store with state and actions in single function | Simple global state |
| set | set((state) => ({ count: state.count + 1 })) |
Updates state immutably, triggers re-renders | State mutations |
| get | get().propertyName |
Reads current state without subscription | Access state in actions |
| subscribe | store.subscribe((state) => {}) |
Listen to state changes outside components | Logging, persistence |
| persist | persist(store, { name: 'key' }) |
Middleware for localStorage/sessionStorage persistence | State persistence |
| immer | immer((state) => { state.value++ }) |
Allows mutable syntax with Immer library | Nested state updates |
Example: Zustand store with middleware and actions
import { create } from 'zustand';
import { persist, devtools } from 'zustand/middleware';
import { immer } from 'zustand/middleware/immer';
// Basic store
const useCountStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
reset: () => set({ count: 0 }),
}));
// Advanced store with TypeScript, persist, and devtools
interface BearState {
bears: number;
fish: number;
actions: {
addBear: () => void;
removeBear: () => void;
addFish: (amount: number) => void;
};
}
const useBearStore = create<BearState>()(
devtools(
persist(
immer((set, get) => ({
bears: 0,
fish: 0,
actions: {
addBear: () => set((state) => { state.bears += 1; }),
removeBear: () => set((state) => { state.bears -= 1; }),
addFish: (amount) => set((state) => {
state.fish += amount;
}),
},
})),
{ name: 'bear-storage' }
)
)
);
// Separate selectors for optimization
const useBears = () => useBearStore((state) => state.bears);
const useActions = () => useBearStore((state) => state.actions);
// Component usage
function BearCounter() {
const bears = useBears();
const { addBear, removeBear } = useActions();
return (
<div>
<h1>Bears: {bears}</h1>
<button onClick={addBear}>Add Bear</button>
<button onClick={removeBear}>Remove Bear</button>
</div>
);
}
// Async actions
const useUserStore = create((set, get) => ({
user: null,
loading: false,
fetchUser: async (id) => {
set({ loading: true });
const response = await fetch(`/api/users/${id}`);
const user = await response.json();
set({ user, loading: false });
},
}));
3. Context API Provider Pattern
| Feature | Syntax | Description | Use Case |
|---|---|---|---|
| createContext | const Context = createContext(defaultValue) |
Creates context object for sharing data | Global state initialization |
| Provider | <Context.Provider value={data}> |
Makes context value available to descendants | Wrapping component tree |
| useContext | const value = useContext(Context) |
Consumes context value in child components | Access shared state |
| Custom Hook Pattern | const useAuth = () => useContext(AuthContext) |
Encapsulates context with validation and helpers | Better DX, type safety |
| Multiple Contexts | <Theme><Auth><App/></Auth></Theme> |
Compose multiple context providers | Separation of concerns |
Example: Context API with TypeScript and custom hook
// AuthContext.tsx
import { createContext, useContext, useState, ReactNode } from 'react';
interface User {
id: string;
name: string;
email: string;
}
interface AuthContextType {
user: User | null;
login: (email: string, password: string) => Promise<void>;
logout: () => void;
isAuthenticated: boolean;
}
const AuthContext = createContext<AuthContextType | undefined>(undefined);
// Custom hook with validation
export function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within AuthProvider');
}
return context;
}
// Provider component
export function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const login = async (email: string, password: string) => {
const response = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify({ email, password }),
});
const userData = await response.json();
setUser(userData);
};
const logout = () => {
setUser(null);
};
const value = {
user,
login,
logout,
isAuthenticated: !!user,
};
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
// Usage in component
function Profile() {
const { user, logout, isAuthenticated } = useAuth();
if (!isAuthenticated) {
return <div>Please login</div>;
}
return (
<div>
<h1>Welcome, {user?.name}</h1>
<button onClick={logout}>Logout</button>
</div>
);
}
// App.tsx with provider
function App() {
return (
<AuthProvider>
<Profile />
</AuthProvider>
);
}
Note: Context API re-renders all consumers when value changes. For performance-critical apps,
split contexts or use useMemo on the value object.
4. Recoil Atom Selector Implementation
| Feature | Syntax | Description | Use Case |
|---|---|---|---|
| atom | atom({ key: 'id', default: value }) |
Unit of state that components can subscribe to | Shared state pieces |
| selector | selector({ key: 'id', get: ({ get }) => {} }) |
Derived state from atoms or other selectors | Computed values |
| useRecoilState | const [val, setVal] = useRecoilState(atom) |
Read and write atom state (like useState) | Read/write state |
| useRecoilValue | const val = useRecoilValue(atom) |
Read-only access to atom/selector value | Read-only state |
| useSetRecoilState | const setVal = useSetRecoilState(atom) |
Write-only access without subscribing to changes | Write-only operations |
| atomFamily | atomFamily({ key: 'id', default: (param) => {} }) |
Creates dynamic atoms based on parameters | List items, dynamic data |
| selectorFamily | selectorFamily({ key: 'id', get: (param) => {} }) |
Creates parameterized selectors | Filtered/transformed data |
Example: Recoil atoms, selectors, and families
import {
atom,
selector,
atomFamily,
selectorFamily,
useRecoilState,
useRecoilValue
} from 'recoil';
// Basic atom
const textState = atom({
key: 'textState',
default: '',
});
// Selector for derived state
const charCountState = selector({
key: 'charCountState',
get: ({ get }) => {
const text = get(textState);
return text.length;
},
});
// Async selector
const currentUserQuery = selector({
key: 'currentUserQuery',
get: async ({ get }) => {
const userId = get(currentUserIdState);
const response = await fetch(`/api/users/${userId}`);
return response.json();
},
});
// Atom family for dynamic state
const todoState = atomFamily({
key: 'todoState',
default: (id) => ({
id,
text: '',
completed: false,
}),
});
// Selector family for filtered data
const filteredTodosSelector = selectorFamily({
key: 'filteredTodosSelector',
get: (filter) => ({ get }) => {
const todos = get(todoListState);
switch (filter) {
case 'completed':
return todos.filter(todo => todo.completed);
case 'active':
return todos.filter(todo => !todo.completed);
default:
return todos;
}
},
});
// Component usage
function TextInput() {
const [text, setText] = useRecoilState(textState);
const charCount = useRecoilValue(charCountState);
return (
<div>
<input value={text} onChange={(e) => setText(e.target.value)} />
<p>Character Count: {charCount}</p>
</div>
);
}
// App with RecoilRoot
import { RecoilRoot } from 'recoil';
function App() {
return (
<RecoilRoot>
<TextInput />
</RecoilRoot>
);
}
5. MobX Observable State Trees
| Feature | Syntax | Description | Use Case |
|---|---|---|---|
| makeObservable | makeObservable(this, { prop: observable }) |
Makes class properties reactive | Class-based stores |
| makeAutoObservable | makeAutoObservable(this) |
Automatically makes all properties observable | Simplified store setup |
| observable | observable({ prop: value }) |
Creates observable object/array/map | Object state |
| computed | get computed() { return this.x * 2 } |
Derived values automatically recalculated | Calculated properties |
| action | @action method() { this.value = x } |
Functions that modify state | State mutations |
| reaction | reaction(() => data, (data) => {}) |
Side effects triggered by observable changes | Logging, persistence |
| observer | observer(Component) |
Makes React component reactive to observables | Component observation |
Example: MobX store with observer components
import { makeAutoObservable, runInAction } from 'mobx';
import { observer } from 'mobx-react-lite';
// Store class
class TodoStore {
todos = [];
filter = 'all';
constructor() {
makeAutoObservable(this);
}
// Computed property
get filteredTodos() {
switch (this.filter) {
case 'active':
return this.todos.filter(todo => !todo.completed);
case 'completed':
return this.todos.filter(todo => todo.completed);
default:
return this.todos;
}
}
get completedCount() {
return this.todos.filter(todo => todo.completed).length;
}
// Actions
addTodo(text) {
this.todos.push({
id: Date.now(),
text,
completed: false,
});
}
toggleTodo(id) {
const todo = this.todos.find(t => t.id === id);
if (todo) {
todo.completed = !todo.completed;
}
}
setFilter(filter) {
this.filter = filter;
}
// Async action
async fetchTodos() {
const response = await fetch('/api/todos');
const todos = await response.json();
runInAction(() => {
this.todos = todos;
});
}
}
// Create store instance
const todoStore = new TodoStore();
// Observer component - automatically re-renders on observable changes
const TodoList = observer(() => {
const { filteredTodos, addTodo, toggleTodo } = todoStore;
return (
<div>
<ul>
{filteredTodos.map(todo => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleTodo(todo.id)}
/>
{todo.text}
</li>
))}
</ul>
<button onClick={() => addTodo('New Todo')}>Add</button>
</div>
);
});
// React Context for store
import { createContext, useContext } from 'react';
const StoreContext = createContext(todoStore);
export const useStore = () => useContext(StoreContext);
6. SWR Data Fetching Caching
| Feature | Syntax | Description | Use Case |
|---|---|---|---|
| useSWR | const { data, error } = useSWR(key, fetcher) |
Hook for data fetching with built-in caching | API data fetching |
| mutate | mutate(key, data, options) |
Manually update cache and revalidate | Optimistic updates |
| useSWRMutation | const { trigger } = useSWRMutation(key, fetcher) |
Hook for mutations (POST, PUT, DELETE) | Data modifications |
| SWRConfig | <SWRConfig value={{ options }}> |
Global configuration provider | Default settings |
| Revalidation | { revalidateOnFocus: true } |
Auto-refetch on window focus, reconnect, interval | Fresh data strategy |
| Pagination | useSWRInfinite(getKey, fetcher) |
Infinite loading/pagination hook | List pagination |
Example: SWR with mutations and optimistic updates
import useSWR, { mutate, useSWRConfig } from 'swr';
import useSWRMutation from 'swr/mutation';
// Fetcher function
const fetcher = (url) => fetch(url).then(res => res.json());
// Basic usage
function UserProfile({ userId }) {
const { data, error, isLoading, mutate } = useSWR(
`/api/users/${userId}`,
fetcher,
{
revalidateOnFocus: true,
revalidateOnReconnect: true,
dedupingInterval: 5000,
}
);
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<div>
<h1>{data.name}</h1>
<button onClick={() => mutate()}>Refresh</button>
</div>
);
}
// Mutation with optimistic update
async function updateUser(url, { arg }) {
await fetch(url, {
method: 'PUT',
body: JSON.stringify(arg),
});
}
function EditProfile({ userId }) {
const { trigger } = useSWRMutation(`/api/users/${userId}`, updateUser);
const handleUpdate = async (updates) => {
// Optimistic update
mutate(
`/api/users/${userId}`,
(current) => ({ ...current, ...updates }),
false // Don't revalidate yet
);
try {
await trigger(updates);
} catch (error) {
// Revert on error
mutate(`/api/users/${userId}`);
}
};
return <button onClick={() => handleUpdate({ name: 'New Name' })}>Update</button>;
}
// Global configuration
import { SWRConfig } from 'swr';
function App() {
return (
<SWRConfig
value={{
refreshInterval: 30000,
fetcher: (url) => fetch(url).then(res => res.json()),
onError: (error) => console.error(error),
}}
>
<UserProfile userId="123" />
</SWRConfig>
);
}
// Infinite loading
import useSWRInfinite from 'swr/infinite';
function UserList() {
const getKey = (pageIndex, previousPageData) => {
if (previousPageData && !previousPageData.hasMore) return null;
return `/api/users?page=${pageIndex}&limit=10`;
};
const { data, size, setSize, isLoading } = useSWRInfinite(getKey, fetcher);
const users = data ? data.flatMap(page => page.users) : [];
const hasMore = data?.[data.length - 1]?.hasMore;
return (
<div>
{users.map(user => <div key={user.id}>{user.name}</div>)}
{hasMore && (
<button onClick={() => setSize(size + 1)}>Load More</button>
)}
</div>
);
}
State Management Comparison
| Library | Best For | Learning Curve | Bundle Size |
|---|---|---|---|
| Redux Toolkit | Complex apps, DevTools, middleware | Medium | ~12KB |
| Zustand | Simple API, minimal boilerplate | Low | ~1KB |
| Context API | Small apps, theme/auth | Low | Built-in |
| Recoil | Atomic state, derived values | Medium | ~21KB |
| MobX | OOP style, automatic tracking | Medium | ~16KB |
| SWR | Server state, caching, revalidation | Low | ~5KB |