State Management Evolution
The frontend world has moved beyond Redux. Modern solutions are simpler, more performant, and better suited to different use cases.
Zustand - Simple & Scalable
import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware';
interface CartState {
items: CartItem[];
total: number;
addItem: (item: CartItem) => void;
removeItem: (id: string) => void;
clearCart: () => void;
}
export const useCartStore = create()(
devtools(
persist(
(set, get) => ({
items: [],
total: 0,
addItem: (item) => set((state) => ({
items: [...state.items, item],
total: state.total + item.price
})),
removeItem: (id) => set((state) => {
const item = state.items.find(i => i.id === id);
return {
items: state.items.filter(i => i.id !== id),
total: state.total - (item?.price || 0)
};
}),
clearCart: () => set({ items: [], total: 0 })
}),
{ name: 'cart-storage' }
)
)
);
// Usage in component
function CartButton() {
const itemCount = useCartStore((state) => state.items.length);
return ;
} Jotai - Atomic State
import { atom, useAtom, useAtomValue, useSetAtom } from 'jotai';
import { atomWithStorage } from 'jotai/utils';
// Define atoms
const userAtom = atom(null);
const themeAtom = atomWithStorage('theme', 'light');
// Derived atoms
const isLoggedInAtom = atom((get) => get(userAtom) !== null);
// Async atom
const userProfileAtom = atom(async (get) => {
const user = get(userAtom);
if (!user) return null;
const response = await fetch(`/api/users/${user.id}/profile`);
return response.json();
});
// Write-only atom for actions
const loginAtom = atom(
null,
async (get, set, credentials: { email: string; password: string }) => {
const response = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify(credentials)
});
const user = await response.json();
set(userAtom, user);
}
);
// Usage
function UserStatus() {
const user = useAtomValue(userAtom);
const isLoggedIn = useAtomValue(isLoggedInAtom);
const [theme, setTheme] = useAtom(themeAtom);
const login = useSetAtom(loginAtom);
return (...);
} Redux Toolkit - When You Need It
import { createSlice, configureStore, createAsyncThunk } from '@reduxjs/toolkit';
// Async thunk
export const fetchUsers = createAsyncThunk(
'users/fetch',
async () => {
const response = await fetch('/api/users');
return response.json();
}
);
const usersSlice = createSlice({
name: 'users',
initialState: {
items: [] as User[],
status: 'idle' as 'idle' | 'loading' | 'succeeded' | 'failed',
error: null as string | null
},
reducers: {
userAdded: (state, action) => {
state.items.push(action.payload);
}
},
extraReducers: (builder) => {
builder
.addCase(fetchUsers.pending, (state) => {
state.status = 'loading';
})
.addCase(fetchUsers.fulfilled, (state, action) => {
state.status = 'succeeded';
state.items = action.payload;
})
.addCase(fetchUsers.rejected, (state, action) => {
state.status = 'failed';
state.error = action.error.message ?? null;
});
}
});TanStack Query - Server State
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
// Fetch data with caching
function useUsers() {
return useQuery({
queryKey: ['users'],
queryFn: () => fetch('/api/users').then(r => r.json()),
staleTime: 5 * 60 * 1000, // 5 minutes
});
}
// Mutations with optimistic updates
function useCreateUser() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (newUser: CreateUserDto) =>
fetch('/api/users', {
method: 'POST',
body: JSON.stringify(newUser)
}).then(r => r.json()),
onMutate: async (newUser) => {
await queryClient.cancelQueries({ queryKey: ['users'] });
const previous = queryClient.getQueryData(['users']);
queryClient.setQueryData(['users'], (old: User[]) => [
...old,
{ ...newUser, id: 'temp' }
]);
return { previous };
},
onError: (err, newUser, context) => {
queryClient.setQueryData(['users'], context?.previous);
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['users'] });
}
});
}Choosing the Right Solution
| Solution | Best For |
|---|---|
| Zustand | Simple global state, easy migration from Redux |
| Jotai | Atomic state, fine-grained updates |
| Redux Toolkit | Complex state logic, middleware needs |
| TanStack Query | Server state, caching, sync |
| React Context | Theme, auth, small shared state |
Conclusion
Match your state management to your needs. Use TanStack Query for server state, lightweight solutions like Zustand for client state, and Context for simple sharing.