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

Когда управление состоянием в приложении Next становится сложным, проблема быстро растёт. Традиционные хуки вроде useState помогают, но порождают prop drilling — передачу данных и функций через множество компонентов вниз по дереву.
Лучше разделять логику управления состоянием и сами компоненты: хранить и обновлять состояние из любой части приложения. Ниже мы пройдём пошагово, как использовать Context API, строя простое todo‑приложение.
Кого рассчитано и что нужно знать
- Базовые знания современного JavaScript (операторы, стрелочные функции).
- Понимание useState и деструктуризации массивов/объектов в JavaScript.
- Node v16.8 или новее и опыт работы с npm или yarn.
Готовый проект можно найти в репозитории на GitHub — используйте ветки starter и context для сравнения.
Понимание состояния приложения (простыми словами)
Состояние приложения — это совокупность данных, которые приложение «знает» в конкретный момент времени: ввод пользователя, данные с сервера, флаги интерфейса и т. д.
Пример: счётчик может быть в следующих состояниях:
- стандартное значение (0);
- увеличенное на единицу;
- уменьшенное на единицу;
- сброшенное в значение по умолчанию.
Компонент React подписывается на изменения состояния. Взаимодействие пользователя (клики, ввод) вызывает обновления состояния, а компонент перерисовывается.
Ниже — минимальный пример счётчика, чтобы напомнить базовый паттерн useState:
const [counter, setCounter] = useState(0);
return (
{counter}
);
Установка и старт проекта
Репозиторий содержит две ветки: starter (UI и заготовка) и context (готовое решение).
Клонирование starter
Выполните в терминале:
git clone -b starter https://github.com/makeuseofcode/Next.js-CRUD-todo-app.gitУстановите зависимости и запустите dev‑сервер:
yarn && yarn devИли:
npm i && npm run devЕсли всё корректно, интерфейс должен открыться в браузере:
Логика: как работает Context API в приложении
Context API позволяет централизовать и шарить состояние между компонентами, избавляя от проп-дриллинга.
Шаг 1: создать и экспортировать контекст
Создайте папку src/app/context и файл todo.context.jsx внутри неё. Импортируйте createContext из react и создайте контекст:
import { createContext} from "react";
const TodoContext = createContext();Затем создайте кастомный хук useTodoContext, который возвращает контекст для удобного использования в компонентах:
export const useTodoContext = () => useContext(TodoContext);(Примечание: в файле требуется также импортировать useContext из react.)
Шаг 2: создать и управлять состояниями
Для CRUD‑операций нужны состояния и провайдер, который будет их предоставлять:
const TodoContextProvider = ({ children }) => {
const [task, setTask] = useState("");
const [tasks, setTasks] = useState([]);
return {children} ;
};
export default TodoContextProvider;Добавьте функцию handleTodoInput, которая обновляет task при вводе:
const handleTodoInput = (input) => setTask(input);Функция createTask — добавляет новую задачу и генерирует ей простой случайный id:
const createTask = (e) => {
e.preventDefault();
setTasks([
{
id: Math.trunc(Math.random() * 1000 + 1),
task,
},
...tasks,
]);
};Функция обновления задачи updateTask обновляет текст задачи по id:
const updateTask = (id, updateText) =>
setTasks(tasks.map((t) => (t.id === id ? { ...t, task: updateText } : t)));Удаление задачи deleteTask фильтрует список по id:
const deleteTask = (id) => setTasks(tasks.filter((t) => t.id !== id));Шаг 3: передать состояния и обработчики в Provider
Добавьте созданные переменные и функции в value провайдера, чтобы компоненты могли их использовать:
return (
{children}
);Шаг 4: область видимости контекста
Чтобы контекст был доступен в приложении, оберните верхний компонент провайдером. В src/app/page.jsx — оберните Todos:
;
;Шаг 5: использовать контекст в компонентах
В компоненте src/app/components/Todos.jsx получите значения через useTodoContext:
const { task, tasks, handleTodoInput, createTask } = useTodoContext();Обновите форму, чтобы она использовала createTask при submit и handleTodoInput при вводе:
Шаг 6: рендер задач в UI
Создайте компонент src/app/components/Todo.jsx для одной задачи. Внутри используйте updateTask и deleteTask из контекста:
import React, { useState } from "react";
import { useTodoContext } from "../context/todo.context";
const Todo = ({ task }) => {
const { updateTask, deleteTask } = useTodoContext();
// isEdit state tracks when a task is in edit mode
const [isEdit, setIsEdit] = useState(false);
return (
{isEdit ? ( updateTask(task.id, e.target.value)} /> ) :
({task.task} )}
);
};
export default Todo;Чтобы отрисовать компонент Todo для каждой задачи, в Todos.jsx добавьте map по tasks:
{tasks && (
{tasks.map((task, i) => ( ))}
)}Проверьте приложение в браузере — оно должно работать как ожидается.
Сохранение задач в localStorage
После перезагрузки страницы задачи теряются. Исправить это можно, записывая список задач в localStorage браузера и восстанавливая его при инициализации.
Ключевые шаги (без кода, чтобы не дублировать уже существующие фрагменты):
- при старте провайдера (в useEffect с пустым массивом зависимостей) прочитать список задач из localStorage и установить в state;
- при изменении tasks (useEffect с зависимостью [tasks]) сериализовать массив в JSON и писать в localStorage;
- учитывать ошибку парсинга и использовать безопасный дефолт [].
Важно: localStorage доступен только в браузере — в Next.js убедитесь, что код чтения/записи выполняется на клиенте.
Важные замечания и хорошие практики
- Генерация id через Math.random подходит для демо, но не для продакшена. Для универсальности используйте UUID (например, uuid) или id от сервера.
- Не храните большие объёмы данных в localStorage — это синхронный API и может блокировать UI при большом объёме.
- Валидация ввода и обработка ошибок обязательны: не полагайтесь только на required в input.
Важно: Context не заменяет архитектуру — он удобен для шаринга состояния, но не всегда лучше Redux/Zustand.
Когда Context не подходит (примеры и контрпример)
Масштабное приложение с тяжёлыми вычислениями и большим количеством подписчиков: при частых обновлениях всего провайдера перерисуются все подписчики — возможны проблемы с производительностью.
Сложная логика с побочными эффектами, транзакциями, undo/redo или сложными селекторами: лучше использовать state management с селекторами/мемоизацией или сторонние библиотеки.
Контрпример: если у вас десятки тысяч элементов в списке и частые обновления отдельных элементов, Context может привести к лишним перерисовкам. В таких случаях стоит рассмотреть локальные состояния на уровне элементов или библиотеку со строчечной подпиской (например, Zustand, Jotai) или Redux с Reselect.
Альтернативные подходы (кратко)
- Redux — хорош для больших приложений с предсказуемостью состояния, middlewares и инструментами отладки.
- Zustand — лёгкая библиотека с подпиской на части состояния, простая настройка и хорошая производительность.
- Jotai/Recoil — атомарный подход к состоянию, удобен для композиции маленьких «кусочков» состояния.
Выбор зависит от требований: масштаб, требования к отладке, команда и привычки.
Ментальные модели и эвристики
- Разделяй ответственность: UI-компоненты — только отображение; бизнес-логика — в провайдере/слое сервиса.
- Минимизируй зону воздействия: чем меньше провайдера охватывает часть, где часто обновляются данные, тем меньше лишних перерисовок.
- Prefer composition over prop drilling: оборачивайте части дерева, которые реально нуждаются в данных.
Факт‑бокс: ключевые концепты
- Context API — механизм для передачи значений через дерево компонентов без prop drilling.
- Provider — компонент, который предоставляет значение контекста всем детям.
- useContext — хук для доступа к значению контекста.
- CRUD — Create, Read, Update, Delete: основные операции над задачами.
- localStorage — синхронное хранилище в браузере для простого персистирования данных.
Мини‑методология внедрения Context в проект
- Определите модель данных (какие поля у задачи).
- Создайте контекст и провайдер в отдельной папке (src/app/context).
- Инкапсулируйте все операции (handleInput, create, update, delete) в провайдере.
- Используйте кастомный хук для доступа к контексту из компонентов.
- Добавьте persistance (localStorage или API) и эффекты для синхронизации.
- Напишите простые тесты/acceptance для CRUD сценариев.
Чек‑лист по ролям
Разработчик:
- Выделить контекст и провайдер в отдельный модуль.
- Написать unit‑тесты для create/update/delete логики.
- Реализовать сохранение в localStorage и обработку ошибок.
Код‑ревьювер:
- Проверить области перерисовки и потенциальные лишние renders.
- Оценить необходимость мемоизации или разделения провайдера.
Product Manager / ТЗ:
- Уточнить требования по persistency (localStorage vs сервер).
- Определить ожидаемое поведение при конфликте обновлений.
Критерии приёмки
- Можно создать, прочитать, обновить и удалить задачу.
- Список задач сохраняется после перезагрузки страницы (localStorage).
- UI не ломается при пустых или некорректных данных.
Примеры тестов (acceptance)
- Создать задачу через форму — она появляется в списке и в localStorage.
- Отредактировать задачу — изменения видны и сохраняются.
- Удалить задачу — элемент исчезает и не присутствует в localStorage.
- Перезагрузить страницу — ранее добавленные задачи остаются.
Итог
Context API — удобный инструмент для упрощения распространённого паттерна prop drilling и централизации состояния в небольших и средних приложениях. Для сложных, высоконагруженных систем стоит рассмотреть специализированные библиотеки или гибридные архитектуры. В этом руководстве вы получили практическую реализацию todo‑приложения с Context, советы по персистенции в localStorage и рекомендации по улучшению производительности.
Краткие рекомендации для следующего шага: вынесите логику работы с localStorage в отдельный хук (usePersistedState), замените генерацию id на UUID в продакшене и добавьте тесты на CRUD‑операции.