Modern React State Management in 2024: From Context to Server Components
Explore modern state management patterns in React and Next.js applications. Learn how to effectively use Context, Zustand, Jotai, and Server Components.
Introduction
State management in React has evolved significantly. Let's explore modern patterns with practical examples.
Local State Management
useState with TypeScript
interface Todo {
id: string;
title: string;
completed: boolean;
}
function TodoList() {
const [todos, setTodos] = useState<Todo[]>([]);
const addTodo = (title: string) => {
setTodos((prev) => [
...prev,
{
id: crypto.randomUUID(),
title,
completed: false,
},
]);
};
return (
<div>
<AddTodoForm onAdd={addTodo} />
{todos.map((todo) => (
<TodoItem key={todo.id} todo={todo} />
))}
</div>
);
}
useReducer for Complex State
type TodoAction =
| { type: "ADD_TODO"; payload: string }
| { type: "TOGGLE_TODO"; payload: string }
| { type: "DELETE_TODO"; payload: string };
function todoReducer(state: Todo[], action: TodoAction): Todo[] {
switch (action.type) {
case "ADD_TODO":
return [
...state,
{
id: crypto.randomUUID(),
title: action.payload,
completed: false,
},
];
case "TOGGLE_TODO":
return state.map((todo) =>
todo.id === action.payload
? { ...todo, completed: !todo.completed }
: todo
);
case "DELETE_TODO":
return state.filter((todo) => todo.id !== action.payload);
default:
return state;
}
}
Modern Global State Management
Zustand Example
import create from "zustand";
interface TodoStore {
todos: Todo[];
addTodo: (title: string) => void;
toggleTodo: (id: string) => void;
deleteTodo: (id: string) => void;
}
const useTodoStore = create<TodoStore>((set) => ({
todos: [],
addTodo: (title) =>
set((state) => ({
todos: [
...state.todos,
{ id: crypto.randomUUID(), title, completed: false },
],
})),
toggleTodo: (id) =>
set((state) => ({
todos: state.todos.map((todo) =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
),
})),
deleteTodo: (id) =>
set((state) => ({
todos: state.todos.filter((todo) => todo.id !== id),
})),
}));
Jotai for Atomic State
import { atom, useAtom } from "jotai";
const todosAtom = atom<Todo[]>([]);
const filteredTodosAtom = atom((get) => {
const todos = get(todosAtom);
const filter = get(filterAtom);
switch (filter) {
case "completed":
return todos.filter((t) => t.completed);
case "active":
return todos.filter((t) => !t.completed);
default:
return todos;
}
});
function TodoList() {
const [todos, setTodos] = useAtom(todosAtom);
const [filteredTodos] = useAtom(filteredTodosAtom);
return (
<div>
{filteredTodos.map((todo) => (
<TodoItem key={todo.id} todo={todo} />
))}
</div>
);
}
Server Components and State
React Server Components
// app/todos/page.tsx
async function TodoList() {
const todos = await prisma.todo.findMany();
return (
<div>
<AddTodoForm />
{todos.map((todo) => (
<TodoItem key={todo.id} todo={todo} />
))}
</div>
);
}
// Client Component for Interactivity
("use client");
function AddTodoForm() {
const [title, setTitle] = useState("");
return (
<form
action={async (formData) => {
await createTodo(formData);
setTitle("");
}}
>
<input
value={title}
onChange={(e) => setTitle(e.target.value)}
name="title"
/>
<button type="submit">Add</button>
</form>
);
}
Hybrid Approach with Server Actions
// Server Action
"use server";
async function toggleTodo(id: string) {
await prisma.todo.update({
where: { id },
data: { completed: { not: true } },
});
}
// Client Component
("use client");
function TodoItem({ todo }: { todo: Todo }) {
const [optimisticCompleted, setOptimisticCompleted] = useState(
todo.completed
);
return (
<div>
<input
type="checkbox"
checked={optimisticCompleted}
onChange={async () => {
setOptimisticCompleted(!optimisticCompleted);
await toggleTodo(todo.id);
}}
/>
<span>{todo.title}</span>
</div>
);
}
Performance Optimization
Memoization Patterns
const MemoizedTodoItem = memo(function TodoItem({
todo,
onToggle,
}: TodoItemProps) {
return (
<div>
<input
type="checkbox"
checked={todo.completed}
onChange={() => onToggle(todo.id)}
/>
<span>{todo.title}</span>
</div>
);
});
const useToggleTodo = (id: string) => {
return useCallback(() => {
toggleTodo(id);
}, [id]);
};
Conclusion
Modern React state management offers multiple approaches, each with its own use cases:
- Use local state for simple components
- Consider Zustand or Jotai for global state
- Leverage Server Components for data-heavy features
- Combine approaches for optimal performance
Remember to:
- Keep state as close to where it's used as possible
- Use Server Components when appropriate
- Implement optimistic updates for better UX
- Optimize performance with memoization
The future of React state management is hybrid, combining client and server state effectively.