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.