Redux was once the go-to solution for React state management, but the ecosystem has evolved dramatically. As a senior developer, I’ve seen teams struggle with Redux’s complexity when simpler solutions would work better. Let’s explore modern state management patterns that are more intuitive, performant, and maintainable.
Table of Contents
- Why Move Beyond Redux?
- React Context API
- Modern State Management Libraries
- When to Use Each Solution
- Migration Strategies
- Performance Considerations
- Real-World Examples
- Best Practices
- Next Steps
Why Move Beyond Redux?
Redux has several limitations that modern alternatives address:
- Boilerplate overload - Actions, reducers, selectors, middleware
- Complex setup - Store configuration, dev tools, middleware
- Performance overhead - Unnecessary re-renders and complexity
- Learning curve - Hard for junior developers to grasp
- Bundle size - Large footprint for simple state needs
Modern solutions offer:
- Less boilerplate - Write less code, do more
- Better performance - Automatic optimizations
- Easier learning curve - Intuitive APIs
- Smaller bundles - Tree-shakable and lightweight
React Context API
Basic Context Pattern
// ❌ BAD: Prop drilling
function App() {
const [user, setUser] = useState(null);
const [theme, setTheme] = useState("light");
return (
<div>
<Header user={user} theme={theme} />
<Main user={user} theme={theme} />
<Footer user={user} theme={theme} />
</div>
);
}
// ✅ GOOD: Context API
const AppContext = createContext();
function AppProvider({ children }) {
const [user, setUser] = useState(null);
const [theme, setTheme] = useState("light");
const value = {
user,
setUser,
theme,
setTheme,
};
return <AppContext.Provider value={value}>{children}</AppContext.Provider>;
}
function useApp() {
const context = useContext(AppContext);
if (!context) {
throw new Error("useApp must be used within AppProvider");
}
return context;
}
function App() {
return (
<AppProvider>
<Header />
<Main />
<Footer />
</AppProvider>
);
}
Optimized Context with Memoization
// Prevent unnecessary re-renders
const AppContext = createContext();
function AppProvider({ children }) {
const [user, setUser] = useState(null);
const [theme, setTheme] = useState("light");
// Memoize context value to prevent unnecessary re-renders
const value = useMemo(
() => ({
user,
setUser,
theme,
setTheme,
}),
[user, theme],
);
return <AppContext.Provider value={value}>{children}</AppContext.Provider>;
}
// Split contexts for better performance
const UserContext = createContext();
const ThemeContext = createContext();
function AppProvider({ children }) {
const [user, setUser] = useState(null);
const [theme, setTheme] = useState("light");
return (
<UserContext.Provider value={{ user, setUser }}>
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
</UserContext.Provider>
);
}
Modern State Management Libraries
Zustand
Zustand is a small, fast, and scalable state management solution.
import { create } from "zustand";
// Simple store
const useStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
reset: () => set({ count: 0 }),
}));
// Usage
function Counter() {
const { count, increment, decrement, reset } = useStore();
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
<button onClick={reset}>Reset</button>
</div>
);
}
Advanced Zustand Patterns
// TypeScript support
interface UserStore {
user: User | null;
loading: boolean;
error: string | null;
fetchUser: (id: string) => Promise<void>;
updateUser: (user: User) => void;
logout: () => void;
}
const useUserStore = create<UserStore>((set, get) => ({
user: null,
loading: false,
error: null,
fetchUser: async (id: string) => {
set({ loading: true, error: null });
try {
const user = await api.getUser(id);
set({ user, loading: false });
} catch (error) {
set({ error: error.message, loading: false });
}
},
updateUser: (user: User) => {
set({ user });
},
logout: () => {
set({ user: null, error: null });
},
}));
// Selectors for performance
function UserProfile() {
const user = useUserStore((state) => state.user);
const loading = useUserStore((state) => state.loading);
const fetchUser = useUserStore((state) => state.fetchUser);
// Only re-renders when user or loading changes
if (loading) return <div>Loading...</div>;
if (!user) return <div>No user</div>;
return <div>Hello, {user.name}!</div>;
}
Jotai
Jotai is an atomic state management library that’s perfect for React.
import { atom, useAtom } from "jotai";
// Atomic state
const countAtom = atom(0);
const userAtom = atom(null);
const themeAtom = atom("light");
// Derived atoms
const doubleCountAtom = atom((get) => get(countAtom) * 2);
const userDisplayNameAtom = atom((get) => {
const user = get(userAtom);
return user ? user.name : "Guest";
});
// Usage
function Counter() {
const [count, setCount] = useAtom(countAtom);
const [doubleCount] = useAtom(doubleCountAtom);
return (
<div>
<p>Count: {count}</p>
<p>Double: {doubleCount}</p>
<button onClick={() => setCount((c) => c + 1)}>+</button>
</div>
);
}
Valtio
Valtio makes state management feel like regular JavaScript objects.
import { proxy, useSnapshot } from "valtio";
// Create a proxy state
const state = proxy({
count: 0,
user: null,
theme: "light",
increment() {
this.count++;
},
setUser(user) {
this.user = user;
},
toggleTheme() {
this.theme = this.theme === "light" ? "dark" : "light";
},
});
// Usage in components
function Counter() {
const snap = useSnapshot(state);
return (
<div>
<p>Count: {snap.count}</p>
<button onClick={() => state.increment()}>+</button>
</div>
);
}
When to Use Each Solution
Context API
Use when:
- Simple global state (theme, user, language)
- Small to medium applications
- Team prefers built-in React solutions
- Minimal state updates
Avoid when:
- Complex state logic
- Frequent state updates
- Performance is critical
Zustand
Use when:
- Need simple, fast state management
- Want minimal boilerplate
- Building medium to large applications
- Need TypeScript support
Avoid when:
- Very simple state (Context API is better)
- Need complex middleware
Jotai
Use when:
- Building with React 18+ features
- Want atomic state management
- Need fine-grained reactivity
- Building complex UIs
Avoid when:
- Simple state needs
- Team prefers traditional stores
Valtio
Use when:
- Want mutable state syntax
- Building prototypes quickly
- Team prefers object-oriented patterns
- Need simple state updates
Avoid when:
- Need strict immutability
- Building large, complex applications
Migration Strategies
From Redux to Zustand
// Redux store
const initialState = {
user: null,
posts: [],
loading: false,
error: null,
};
const userSlice = createSlice({
name: "user",
initialState,
reducers: {
setUser: (state, action) => {
state.user = action.payload;
},
setPosts: (state, action) => {
state.posts = action.payload;
},
setLoading: (state, action) => {
state.loading = action.payload;
},
setError: (state, action) => {
state.error = action.payload;
},
},
});
// Zustand equivalent
const useStore = create((set) => ({
user: null,
posts: [],
loading: false,
error: null,
setUser: (user) => set({ user }),
setPosts: (posts) => set({ posts }),
setLoading: (loading) => set({ loading }),
setError: (error) => set({ error }),
}));
From Context to Zustand
// Context API
const UserContext = createContext();
function UserProvider({ children }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(false);
const fetchUser = async (id) => {
setLoading(true);
const user = await api.getUser(id);
setUser(user);
setLoading(false);
};
return (
<UserContext.Provider value={{ user, loading, fetchUser }}>
{children}
</UserContext.Provider>
);
}
// Zustand equivalent
const useUserStore = create((set) => ({
user: null,
loading: false,
fetchUser: async (id) => {
set({ loading: true });
const user = await api.getUser(id);
set({ user, loading: false });
},
}));
Performance Considerations
Context API Performance
// ❌ BAD: Large context value
const AppContext = createContext();
function AppProvider({ children }) {
const [state, setState] = useState({
user: null,
posts: [],
comments: [],
settings: {},
notifications: [],
// ... many more
});
return (
<AppContext.Provider value={{ state, setState }}>
{children}
</AppContext.Provider>
);
}
// ✅ GOOD: Split contexts
const UserContext = createContext();
const PostsContext = createContext();
const SettingsContext = createContext();
// ✅ BETTER: Use Zustand for complex state
const useStore = create((set) => ({
user: null,
posts: [],
settings: {},
// ... all state in one place
}));
Zustand Performance
// Selective subscriptions
function UserProfile() {
// Only re-renders when user changes
const user = useStore((state) => state.user);
// Only re-renders when posts change
const posts = useStore((state) => state.posts);
return (
<div>
<h1>{user?.name}</h1>
<PostList posts={posts} />
</div>
);
}
// Shallow comparison
const useStore = create((set) => ({
user: null,
posts: [],
setUser: (user) => set({ user }, true), // true = shallow comparison
}));
Real-World Examples
E-commerce Cart with Zustand
const useCartStore = create((set, get) => ({
items: [],
total: 0,
addItem: (product) => {
const { items } = get();
const existingItem = items.find((item) => item.id === product.id);
if (existingItem) {
set({
items: items.map((item) =>
item.id === product.id
? { ...item, quantity: item.quantity + 1 }
: item,
),
});
} else {
set({
items: [...items, { ...product, quantity: 1 }],
});
}
// Recalculate total
get().calculateTotal();
},
removeItem: (productId) => {
const { items } = get();
set({
items: items.filter((item) => item.id !== productId),
});
get().calculateTotal();
},
updateQuantity: (productId, quantity) => {
const { items } = get();
set({
items: items.map((item) =>
item.id === productId
? { ...item, quantity: Math.max(0, quantity) }
: item,
),
});
get().calculateTotal();
},
calculateTotal: () => {
const { items } = get();
const total = items.reduce(
(sum, item) => sum + item.price * item.quantity,
0,
);
set({ total });
},
clearCart: () => set({ items: [], total: 0 }),
}));
// Usage
function Cart() {
const { items, total, addItem, removeItem, updateQuantity } = useCartStore();
return (
<div>
{items.map((item) => (
<CartItem
key={item.id}
item={item}
onRemove={() => removeItem(item.id)}
onUpdateQuantity={(quantity) => updateQuantity(item.id, quantity)}
/>
))}
<div>Total: ${total}</div>
</div>
);
}
Theme Management with Jotai
import { atom, useAtom } from "jotai";
import { atomWithStorage } from "jotai/utils";
// Persistent theme atom
const themeAtom = atomWithStorage("theme", "light");
// Derived atoms
const isDarkAtom = atom((get) => get(themeAtom) === "dark");
const themeColorsAtom = atom((get) => {
const theme = get(themeAtom);
return theme === "dark" ? darkColors : lightColors;
});
// Actions
const toggleThemeAtom = atom(null, (get, set) => {
const currentTheme = get(themeAtom);
set(themeAtom, currentTheme === "light" ? "dark" : "light");
});
// Usage
function ThemeToggle() {
const [isDark] = useAtom(isDarkAtom);
const [, toggleTheme] = useAtom(toggleThemeAtom);
return <button onClick={toggleTheme}>{isDark ? "☀️" : "🌙"}</button>;
}
Best Practices
1. Choose the Right Tool
// Simple global state → Context API
const ThemeContext = createContext();
// Complex state with actions → Zustand
const useStore = create((set) => ({
// complex state and actions
}));
// Atomic state → Jotai
const countAtom = atom(0);
// Mutable state → Valtio
const state = proxy({ count: 0 });
2. Structure Your Stores
// Organize by feature
const useUserStore = create((set) => ({
// User-related state
}));
const useCartStore = create((set) => ({
// Cart-related state
}));
const useSettingsStore = create((set) => ({
// Settings-related state
}));
3. Use Selectors for Performance
// ❌ BAD: Subscribe to entire store
function UserProfile() {
const store = useStore(); // Re-renders on any change
return <div>{store.user?.name}</div>;
}
// ✅ GOOD: Use selectors
function UserProfile() {
const user = useStore((state) => state.user); // Only re-renders when user changes
return <div>{user?.name}</div>;
}
Next Steps
1. Advanced Patterns
- Server State Management with React Query
- Form State Management with React Hook Form
- URL State Management with React Router
2. Performance Optimization
- Use React DevTools Profiler
- Implement React.memo for expensive components
- Use useMemo for expensive calculations
3. Testing State Management
import { renderHook, act } from "@testing-library/react";
import { useStore } from "./store";
test("increments count", () => {
const { result } = renderHook(() => useStore());
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});
4. Related Learning Resources
- React Hooks Patterns - Build custom hooks for state
- TypeScript Patterns - Type-safe state management
- React Performance - Optimize state updates
Conclusion
Modern state management solutions offer significant advantages over Redux for most applications. The key is choosing the right tool for your specific needs.
Key Takeaways for Junior Developers:
- Start simple - Use Context API for basic global state
- Choose the right tool - Each solution has its strengths
- Consider performance - Use selectors and memoization
- Structure your state - Organize by features
- Test your state - Ensure reliability and maintainability
Remember: State management should make your life easier, not harder. Choose solutions that reduce complexity and improve developer experience.
Related Articles:
- React Hooks Patterns - Build custom state hooks
- TypeScript Patterns - Type-safe state management
- React Performance - Optimize state updates
Start building better state management today! 🚀