Управление состоянием в Next.js с Context API — простое To‑Do приложение

Когда управление состоянием в приложении Next становится сложным, легко запутаться. Традиционные хуки вроде useState удобны, но порождают проблему prop drilling — передачу данных и функций через цепочку компонентов вниз. Лучше отделить логику состояния от компонентов и давать к ней доступ из любой части приложения.
В этой статье мы шаг за шагом создадим простое приложение To‑Do с помощью Context API и покажем, как реализовать создание, чтение, обновление и удаление задач, а также как сохранять данные в localStorage.
Что нужно знать заранее
Перед началом убедитесь, что у вас есть:
- Базовые знания современных операторов JavaScript и хука useState в React.
- Понимание деструктуризации массивов и объектов в JavaScript.
- Node.js версии 16.8+ и опыт работы с npm или yarn.
Готовый проект доступен в репозитории GitHub для справки.
Понимание состояния приложения
Состояние приложения — это набор данных, который описывает текущее «состояние» интерфейса в момент времени: ввод пользователя, полученные с API данные, список задач и т. д. Компонент подписывается на изменения состояния, и при взаимодействии пользователя (нажатия кнопок, ввод) происходит обновление этого состояния.
Пример для счётчика: возможные состояния — значение по умолчанию, увеличение, уменьшение и сброс.
Простой компонент счётчика, который обновляет состояние по клику:
import { useState } from "react";
const Counter = () => {
const [counter, setCounter] = useState(0);
return (
{counter}
);
};
export default Counter;Важно: при увеличении количества компонентов и связей между ними хранить состояние локально специфично и быстро приводит к prop drilling.
Установка и подготовка
Репозиторий содержит две ветки: starter и context. Ветка starter — это UI, на котором мы сосредоточимся при добавлении логики. Ветка context — финальный пример.
Клонирование стартового приложения
Откройте терминал и выполните:
git clone -b starter https://github.com/makeuseofcode/Next.js-CRUD-todo-app.gitУстановите зависимости и запустите dev‑сервер:
# с yarn
yarn && yarn dev
# или с npm
npm i && npm run devЕсли всё в порядке, откроется UI в браузере:
Реализация логики с Context API
Context API даёт централизованное место для управления состоянием и методов, которыми могут пользоваться любые дочерние компоненты без передачи пропсов через дерево.
Шаг 1: создание и экспорт Context
Создайте папку src/app/context и внутри файл todo.context.jsx. Импортируйте createContext и создайте контекст:
import React, { createContext, useContext } from "react";
const TodoContext = createContext();
export const useTodoContext = () => useContext(TodoContext);
export default TodoContext;Кратко: createContext создаёт контейнер для значения; хук useTodoContext упрощает доступ к контексту в компонентах.
Шаг 2: состояния и CRUD‑операции
Создайте провайдер, в котором будут состояния и функции для CRUD. Пример базовой структуры провайдера:
import React, { useState } from "react";
import TodoContext from "./todo.context";
const TodoContextProvider = ({ children }) => {
const [task, setTask] = useState("");
const [tasks, setTasks] = useState([]);
const handleTodoInput = (input) => setTask(input);
const createTask = (e) => {
e.preventDefault();
setTasks([
{
id: Math.trunc(Math.random() * 1000 + 1),
task,
},
...tasks,
]);
setTask("");
};
const updateTask = (id, updateText) =>
setTasks(tasks.map((t) => (t.id === id ? { ...t, task: updateText } : t)));
const deleteTask = (id) => setTasks(tasks.filter((t) => t.id !== id));
return (
{children}
);
};
export default TodoContextProvider;Пояснение: мы храним текущую вводимую задачу в task и массив задач в tasks. Все операции (создать/обновить/удалить) модифицируют tasks.
Шаг 3: обёртка приложения провайдером
Чтобы контекст был доступен в любом месте приложения, оберните корневой компонент. В src/app/page.jsx импортируйте провайдер и используйте его:
import TodoContextProvider from "./context/todo.context";
import Todos from "./components/Todos";
export default function Page() {
return (
);
}Шаг 4: использование контекста в компонентах
В src/app/components/Todos.jsx получите значения из контекста:
import { useTodoContext } from "../context/todo.context";
import Todo from "./Todo";
const Todos = () => {
const { task, tasks, handleTodoInput, createTask } = useTodoContext();
return (
{tasks && (
{tasks.map((taskItem, i) => (
))}
)}
);
};
export default Todos;Компонент Todo (src/app/components/Todo.jsx) управляет одной задачей:
import React, { useState } from "react";
import { useTodoContext } from "../context/todo.context";
const Todo = ({ task }) => {
const { updateTask, deleteTask } = useTodoContext();
const [isEdit, setIsEdit] = useState(false);
return (
{isEdit ? (
updateTask(task.id, e.target.value)}
/>
) : (
{task.task}
)}
);
};
export default Todo;Проверьте приложение в браузере — вы должны уметь добавлять, редактировать и удалять задачи.
Сохранение задач в localStorage
При перезагрузке страницы массив tasks сбрасывается. Решение — сохранять задачи в localStorage и восстанавливать их при загрузке.
Пример расширения провайдера с localStorage:
import React, { useState, useEffect } from "react";
import TodoContext from "./todo.context";
const LOCAL_KEY = "my-todos";
const TodoContextProvider = ({ children }) => {
const [task, setTask] = useState("");
const [tasks, setTasks] = useState(() => {
try {
const raw = typeof window !== "undefined" ? localStorage.getItem(LOCAL_KEY) : null;
return raw ? JSON.parse(raw) : [];
} catch (e) {
console.error("Ошибка чтения localStorage", e);
return [];
}
});
useEffect(() => {
try {
localStorage.setItem(LOCAL_KEY, JSON.stringify(tasks));
} catch (e) {
console.error("Ошибка записи в localStorage", e);
}
}, [tasks]);
// ... остальные функции (handleTodoInput, createTask, updateTask, deleteTask)
return (
{children}
);
};
export default TodoContextProvider;Заметки по безопасности и совместимости: localStorage синхронен и ограничен по объёму. Для больших объёмов данных или серверной синхронизации рассмотрите IndexedDB или бэкенд‑хранилище.
Important: при SSR (Server‑Side Rendering) в Next.js нужно проверять наличие window перед обращением к localStorage. Инициализацию состояния с чтением localStorage выполняйте в useEffect или используйте ленивую инициализацию useState с защитой от undefined window.
Когда Context API — хорошее решение, а когда нет
- Подходит, когда вам нужно поделиться данными между многими компонентами без сложной логики изменений.
- Не подходит для очень частых обновлений с большими деревьями компонентов — возможны лишние ререндеры. В таких случаях подумайте о useReducer + memo или внешних решениях (Zustand, Redux).
Контрпример: список в реальном времени с тысячами элементов и частыми изменениями состояния — Context может вызвать заметные перформанс‑проблемы.
Альтернативы и масштабирование
- useReducer + Context — хорош для сложных переходов состояния и уменьшения побочных эффектов.
- Zustand, Jotai — лёгкие стораджи без лишних ререндеров.
- Redux Toolkit — когда нужно централизованное предсказуемое состояние с DevTools и middleware.
Выбор зависит от размеров приложения, частоты обновлений и наличия команды, знакомой с инструментом.
Плейбук: пошаговая методика внедрения Context в проект
- Выделите гранулированные домены состояния (например: todos, auth, ui).
- Создайте отдельные контексты для независимых доменов.
- Минимизируйте shape value: отдавайте только то, что нужно компонентам.
- Используйте memo (useMemo, React.memo) и селекторы, чтобы избежать лишних ререндеров.
- Напишите тесты на основные операции CRUD.
- Добавьте persist (localStorage или backend) по необходимости.
Проверка и критерии приёмки
Критерии приёмки:
- Можно добавить задачу через форму.
- Задача отображается в списке сразу после добавления.
- Можно редактировать существующую задачу; изменения сохраняются.
- Можно удалить задачу и она исчезает из списка.
- После перезагрузки задачи восстанавливаются из localStorage.
- Все операции не приводят к ошибкам в консоли.
Тесты и случаи приёмки
- Добавление: отправить форму с текстом -> задача появляется в начале списка.
- Редактирование: включить режим редактирования, поменять текст, сохранить -> текст изменён.
- Удаление: удалить задачу -> задача отсутствует в DOM и localStorage.
- Сессия: после перезагрузки список совпадает с state до перезагрузки.
Чеклист для роли разработчика и ревьюера
Для разработчика:
- Создал TodoContextProvider с нужными методами.
- Использовал ленивую загрузку из localStorage.
- Обработал случай отсутствия window для SSR.
- Обновления state атомарны и читаемы.
Для ревьюера:
- Проверил отсутствие лишних ререндеров (React DevTools).
- Убедился в корректной сериализации в localStorage.
- Проверил обработку ошибок при чтении/записи в хранилище.
Краткая методология выбора решения (Decision tree)
flowchart TD
A[Нужно поделиться состоянием между компонентами?] -->|Да| B{Частые обновления?}
A -->|Нет| Z[Хранить локально в компоненте]
B -->|Да| C{Объём и сложность данных}
B -->|Нет| D[Context API подходит]
C -->|Простые данные| E[useReducer + Context]
C -->|Сложные/много операций| F[Redux Toolkit или Zustand]Краткий глоссарий
- Context API — механизм React для передачи данных через дерево компонентов без пропсов.
- provider — компонент, который предоставляет значение контекста дочерним компонентам.
- prop drilling — передача пропсов через многие уровни компонентов.
- SSR — server‑side rendering.
Миграционные заметки и лучшие практики
- Если проект станет крупным, выделяйте отдельные контексты по доменам (auth, todos, ui) вместо одного «глобального» контекста.
- Для оптимизации используйте селекторы или мемоизацию значений value:
const value = useMemo(() => ({ task, tasks, handleTodoInput, createTask, updateTask, deleteTask }), [task, tasks]);- Избегайте передачи функций, которые создаются в каждом рендера, без мемоизации.
Риски и смягчения
- Риск: лишние ререндеры при частых обновлениях. Смягчение: мемоизация, разбиение контекста на части.
- Риск: потеря данных при ошибке localStorage. Смягчение: try/catch и fallback в памяти.
Итог
Context API — простой и удобный инструмент для совместного использования состояния между компонентами. Для небольших и средних приложений он часто решает проблему prop drilling и ускоряет разработку. При росте приложения учитывайте производительность и возможные альтернативы, такие как useReducer, Zustand или Redux.
Краткие рекомендации:
- Начинайте с небольших контекстов по доменам.
- Мемоизируйте value, если нужно оптимизировать ререндеры.
- Храните persist‑данные аккуратно и проверяйте SSR.
Спасибо за чтение — теперь вы можете реализовать To‑Do приложение в Next.js с централизованным управлением состоянием и сохранением задач между сессиями.
Похожие материалы
Как сохранить Excel в PDF — быстро и без ошибок
Как открыть и извлечь ISO в Linux
Snapseed: полное руководство по мобильному редактированию
Как выпустить музыку на Spotify и Apple Music
VPN для PS4: настройка и советы