React & Next.js Topic
React State Management Libraries
The Evolution of State Management
Why We Need State Management Libraries
While React's built-in state is great for:
-
Local component state
-
Simple parent-child data flow
-
Limited scope state
We need state management libraries for:
-
Global state shared across many components
-
Complex state logic with multiple updates
-
Server cache state alongside UI state
-
DevTools for debugging and time-travel debugging
-
Performance optimizations for frequent updates
1. Redux Toolkit (Modern Redux)
Redux Toolkit (RTK) is the official, opinionated, batteries-included toolset for efficient Redux development. It addresses Redux's historical boilerplate issues.
Core Concepts
jsx
// 1. Create a Slice (contains reducer logic + actions)
import { createSlice, configureStore } from '@reduxjs/toolkit';
const counterSlice = createSlice({
name: 'counter',
initialState: {
value: 0,
loading: false,
error: null
},
reducers: {
increment: (state) => {
state.value += 1; // ✅ Can "mutate" directly (uses Immer internally)
},
decrement: (state) => {
state.value -= 1;
},
incrementByAmount: (state, action) => {
state.value += action.payload;
},
setLoading: (state, action) => {
state.loading = action.payload;
}
}
});
// Auto-generated action creators
export const { increment, decrement, incrementByAmount, setLoading } = counterSlice.actions;
// 2. Create Store
const store = configureStore({
reducer: {
counter: counterSlice.reducer,
// other reducers...
}
});
// 3. Provide store to React app
import { Provider } from 'react-redux';
function App() {
return (
<Provider store={store}>
<MyComponent />
</Provider>
);
}
// 4. Use in components with React-Redux hooks
import { useSelector, useDispatch } from 'react-redux';
function Counter() {
const count = useSelector((state) => state.counter.value);
const dispatch = useDispatch();
return (
<div>
<span>Count: {count}</span>
<button onClick={() => dispatch(increment())}>+</button>
<button onClick={() => dispatch(decrement())}>-</button>
<button onClick={() => dispatch(incrementByAmount(5))}>+5</button>
</div>
);
}
Async Logic with createAsyncThunk
jsx
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
// Async thunk for API calls
export const fetchUserById = createAsyncThunk(
'users/fetchById',
async (userId, { rejectWithValue }) => {
try {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) throw new Error('Server error!');
return await response.json();
} catch (error) {
return rejectWithValue(error.message);
}
}
);
const usersSlice = createSlice({
name: 'users',
initialState: {
entities: {},
loading: 'idle', // 'idle' | 'pending' | 'succeeded' | 'failed'
error: null
},
reducers: {
// sync reducers...
},
extraReducers: (builder) => {
builder
.addCase(fetchUserById.pending, (state) => {
state.loading = 'pending';
state.error = null;
})
.addCase(fetchUserById.fulfilled, (state, action) => {
state.loading = 'succeeded';
state.entities[action.payload.id] = action.payload;
})
.addCase(fetchUserById.rejected, (state, action) => {
state.loading = 'failed';
state.error = action.payload;
});
}
});
// Usage in component
function UserProfile({ userId }) {
const dispatch = useDispatch();
const { user, loading, error } = useSelector((state) => ({
user: state.users.entities[userId],
loading: state.users.loading,
error: state.users.error
}));
useEffect(() => {
dispatch(fetchUserById(userId));
}, [dispatch, userId]);
if (loading === 'pending') return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
return <div>{user?.name}</div>;
}
RTK Query (Data Fetching & Caching)
jsx
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
// Define API service
export const apiSlice = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
tagTypes: ['User', 'Post'],
endpoints: (builder) => ({
getUsers: builder.query({
query: () => '/users',
providesTags: ['User']
}),
getUserById: builder.query({
query: (id) => `/users/${id}`,
providesTags: (result, error, id) => [{ type: 'User', id }]
}),
createUser: builder.mutation({
query: (userData) => ({
url: '/users',
method: 'POST',
body: userData
}),
invalidatesTags: ['User']
})
})
});
// Auto-generated hooks
export const { useGetUsersQuery, useGetUserByIdQuery, useCreateUserMutation } = apiSlice;
// Setup store
const store = configureStore({
reducer: {
[apiSlice.reducerPath]: apiSlice.reducer,
// other reducers...
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(apiSlice.middleware)
});
// Usage in component
function UsersList() {
const { data: users, isLoading, error } = useGetUsersQuery();
const [createUser, { isLoading: isCreating }] = useCreateUserMutation();
const handleAddUser = async (userData) => {
try {
await createUser(userData).unwrap();
// Cache automatically invalidated, data refetched
} catch (err) {
console.error('Failed to create user:', err);
}
};
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<div>
{users?.map(user => <div key={user.id}>{user.name}</div>)}
<button onClick={() => handleAddUser({ name: 'New User' })}>
{isCreating ? 'Creating...' : 'Add User'}
</button>
</div>
);
}
2. Zustand (Lightweight Alternative)
Zustand is a small, fast, and scalable state management solution with a minimal API.
Basic Usage
jsx
import { create } from 'zustand';
// Create a store
const useCounterStore = create((set, get) => ({
count: 0,
loading: false,
// Actions
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
incrementBy: (amount) => set((state) => ({ count: state.count + amount })),
// Async action
incrementAsync: async (amount) => {
set({ loading: true });
await new Promise(resolve => setTimeout(resolve, 1000));
set((state) => ({ count: state.count + amount, loading: false }));
},
// Using get() to access current state in actions
logCount: () => {
const currentCount = get().count;
console.log('Current count:', currentCount);
}
}));
// Usage in component
function Counter() {
const { count, loading, increment, incrementAsync } = useCounterStore();
return (
<div>
<span>Count: {count}</span>
<button onClick={increment} disabled={loading}>+</button>
<button onClick={() => incrementAsync(5)} disabled={loading}>
{loading ? 'Loading...' : '+5 Async'}
</button>
</div>
);
}
// Selective state picking (prevents unnecessary re-renders)
function CountDisplay() {
const count = useCounterStore((state) => state.count);
return <div>Current count: {count}</div>;
}
Advanced Zustand Patterns
jsx
// Middleware for persistence, devtools, etc.
const useAuthStore = create(
persist(
(set, get) => ({
user: null,
token: null,
login: async (credentials) => {
set({ loading: true });
try {
const response = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify(credentials)
});
const { user, token } = await response.json();
set({ user, token, loading: false, error: null });
} catch (error) {
set({ error: error.message, loading: false });
}
},
logout: () => set({ user: null, token: null, error: null }),
// Computed values
isAuthenticated: () => !!get().token,
}),
{
name: 'auth-storage', // localStorage key
// Only persist these fields
partialize: (state) => ({ user: state.user, token: state.token })
}
)
);
// Slice pattern for larger stores
const createUserSlice = (set, get) => ({
users: [],
addUser: (user) => set((state) => ({
users: [...state.users, user]
})),
removeUser: (id) => set((state) => ({
users: state.users.filter(user => user.id !== id)
}))
});
const createPostSlice = (set, get) => ({
posts: [],
addPost: (post) => set((state) => ({
posts: [...state.posts, post]
}))
});
// Combine slices
const useAppStore = create((...args) => ({
...createUserSlice(...args),
...createPostSlice(...args)
}));
3. Jotai (Atomic State Management)
Jotai takes a atomic approach inspired by React's own useState. It's great for primitive state and derived state.
Basic Usage
jsx
import { atom, useAtom } from 'jotai';
// Primitive atoms
const countAtom = atom(0);
const nameAtom = atom('John');
// Derived atoms (computed state)
const doubledCountAtom = atom((get) => get(countAtom) * 2);
const userInfoAtom = atom((get) => ({
name: get(nameAtom),
count: get(countAtom),
doubled: get(doubledCountAtom)
}));
// Write-only atoms (actions)
const incrementAtom = atom(null, (get, set) => {
set(countAtom, get(countAtom) + 1);
});
// Async atoms
const userAtom = atom(async (get) => {
const userId = get(selectedUserIdAtom);
const response = await fetch(`/api/users/${userId}`);
return response.json();
});
function Counter() {
const [count, setCount] = useAtom(countAtom);
const [doubledCount] = useAtom(doubledCountAtom);
const [, increment] = useAtom(incrementAtom);
return (
<div>
<p>Count: {count}</p>
<p>Doubled: {doubledCount}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
<button onClick={increment}>Increment (action)</button>
</div>
);
}
Advanced Jotai Patterns
jsx
import { atom, useAtom, Provider } from 'jotai';
import { useHydrateAtoms } from 'jotai/utils';
// Atoms with localStorage persistence
const persistentAtom = (key, initialValue) => {
const baseAtom = atom(initialValue);
const persistedAtom = atom(
(get) => get(baseAtom),
(get, set, update) => {
set(baseAtom, update);
localStorage.setItem(key, JSON.stringify(update));
}
);
return persistedAtom;
};
// SSR hydration
const countAtom = atom(0);
function HydrateAtoms({ initialCount, children }) {
useHydrateAtoms([[countAtom, initialCount]]);
return children;
}
function App({ initialCount }) {
return (
<Provider>
<HydrateAtoms initialCount={initialCount}>
<MyComponent />
</HydrateAtoms>
</Provider>
);
}
Comparison & When to Use
| Library | Bundle Size | Learning Curve | Use Cases |
|---|---|---|---|
| Redux Toolkit | ~9kB + React-Redux | Moderate | Large apps, complex state, need DevTools, team projects |
| Zustand | ~1.5kB | Easy | Small-medium apps, simple global state, minimal boilerplate |
| Jotai | ~3kB | Easy | Primitive state, derived state, React-like mental model |
Decision Guide
Choose Redux Toolkit when:
-
Building large, complex applications
-
Need excellent DevTools and time-travel debugging
-
Working in a team with established patterns
-
Need built-in solutions for data fetching (RTK Query)
-
Require middleware ecosystem
Choose Zustand when:
-
Building small to medium applications
-
Want minimal boilerplate and simple API
-
Need a lightweight global state solution
-
Prefer less opinionated structure
Choose Jotai when:
-
Working with lots of primitive state
-
Need fine-grained reactivity
-
Want React-like mental model
-
Need computed/derived state
Modern Recommendation
jsx
// For most new projects today:
// 1. Start with React built-in state + Context
// 2. Add Zustand when you need more than Context
// 3. Use Redux Toolkit for very complex apps or teams
// 4. Consider Jotai for fine-grained reactive state
// Example: Modern stack for a medium app
- React Query/TanStack Query (server state)
- Zustand (client state)
- React Hook Form (forms)
The key is to start simple and only add complexity when you have clear needs. Most applications don't need Redux's full power, and Zustand or even Context + useReducer might be sufficient.
Comments (0)
No comments yet. Be the first to comment!