Создание корзины в Next.js с Context API и useReducer

В интернет-магазине корзина — ключевой компонент: она хранит выбранные товары, обеспечивает добавление/удаление и передаёт данные на страницы оформления заказа. В Next.js для этого удобно использовать Context API для шаринга состояния между компонентами и useReducer для управления сложной логикой изменения состояния.
Краткая идея
- Context API: сервис для передачи данных через дерево компонентов без проп-дриллинга. Определяете контекст, оборачиваете приложение провайдером и читаете значения через useContext.
- useReducer: альтернатива useState для более сложных операций над состоянием. Принимает редьюсер и начальное состояние; возвращает текущее состояние и dispatch.
Определения в одну строку:
- Context API — глобальный контейнер данных для React-компонентов.
- useReducer — паттерн «action → reducer → новый state».
Пример: Страница продукта
Ниже — минимальный компонент страницы товара. Он принимает id, name, price и рендерит кнопку, которая добавляет или удаляет товар из корзины.
export default function Product({id, name, price}) {
return (
{name}
{price}
)
}Обратите внимание: реальная версия компонента будет подключать контекст и переключать текст/поведение кнопки в зависимости от того, есть ли товар в корзине.
Создание контекста корзины
Создайте файл context/cartContext.js и определите контекст с начальным значением. Ниже — полная, минимально необходимая реализация провайдера с редьюсером и экшенами “ADD” и “REMOVE”.
import { createContext, useReducer } from "react";
export const CartContext = createContext({
items: [],
});
const initialState = { items: [] };
const cartReducer = (state, action) => {
const { type, payload } = action;
switch (type) {
case "ADD":
return {
...state,
items: payload.items,
};
case "REMOVE":
return {
...state,
items: payload.items,
};
default:
throw new Error("No case for that type");
}
};
export const CartProvider = ({ children }) => {
const [state, dispatch] = useReducer(cartReducer, initialState);
const addToCart = (product) => {
const updatedCart = [...state.items, product];
dispatch({
type: "ADD",
payload: {
items: updatedCart,
},
});
};
const removeFromCart = (id) => {
const updatedCart = state.items.filter(
(currentProduct) => currentProduct.id !== id
);
dispatch({
type: "REMOVE",
payload: {
items: updatedCart,
},
});
};
const value = {
items: state.items,
addToCart,
removeFromCart,
};
return {children} ;
};Пояснения:
- initialState содержит массив items.
- cartReducer реагирует на экшены “ADD” и “REMOVE” и заменяет массив items в state на тот, который приходит в payload.
- Провайдер экспортирует items, addToCart и removeFromCart через value.
Важно: логика добавления/удаления здесь простая (полная замена массива). При необходимости вы можете менять структуру payload и делать иммутабельные обновления, учитывать количество единиц товара, проверять дубли и т.д.
Регистрация провайдера в приложении
Чтобы значения контекста были доступны по всему приложению, оберните корневой компонент провайдером CartProvider. В примере используется верхний компонент index.js (в Next.js обычно это _app.js):
import { CartProvider } from "../context/cartContext";
function MyApp({ Component, pageProps }) {
return (
);
}
export default MyApp;Потребление контекста в компоненте продукта
В компоненте Product подключите useContext и возьмите items, addToCart и removeFromCart из CartContext. Также удобно отслеживать, есть ли текущий товар в корзине, через useState и useEffect, чтобы переключать поведение кнопки.
import { useContext, useEffect, useState } from "react";
import { CartContext } from "../context/cartContext";
export default function Product({ id, name, price }) {
const { items, addToCart, removeFromCart } = useContext(CartContext);
const [exists, setExists] = useState(false);
useEffect(() => {
const inCart = items.find((item) => item.id === id);
if (inCart) {
setExists(true);
} else {
setExists(false);
}
}, [items, id]);
return (
{name}
{price}
{
exists
?
:
}
);
}Примечания:
- useEffect гарантирует, что при обновлении items мы пересчитываем флаг exists.
- addToCart и removeFromCart приходят из провайдера и выполняют dispatch.
Расширение функциональности
Добавить хранение количества, подсчёт общей суммы и синхронизацию с localStorage или сервером можно на той же основе:
- Вместо простого списка товаров храните объекты { id, name, price, quantity }.
- Экшены редьюсера: “ADD_ONE”, “DECREMENT”, “SET_QUANTITY”, “CLEAR_CART”.
- В addToCart сначала проверяйте, есть ли товар в корзине, и увеличивайте quantity, а не дублируйте запись.
- Для сохранения между сессиями синхронизируйте state с localStorage через useEffect в провайдере.
Фрагмент-идеи (логика, не полный код):
// при добавлении
const addToCart = (product) => {
const exists = state.items.find(i => i.id === product.id);
let updated;
if (exists) {
updated = state.items.map(i => i.id === product.id ? { ...i, quantity: i.quantity + 1 } : i);
} else {
updated = [...state.items, { ...product, quantity: 1 }];
}
dispatch({ type: "SET_ITEMS", payload: { items: updated } });
};Когда такой подход может не подойти
- Очень большой объём состояния с сложными зависимостями и большим числом операций — может быть удобнее использовать специализированные менеджеры состояния (Redux, Zustand или MobX).
- Если требуется сложная синхронизация с сервером в реальном времени (операции с конфликтами, optimistic updates), стоит внедрять дополнительные уровни абстракции.
Альтернативные подходы
- Redux Toolkit: строгая типизация и удобные devtools для сложных приложений.
- Zustand: лёгкий и быстрый для локального глобального состояния.
- Apollo Client (для GraphQL): кеширование и синхронизация с сервером.
Выбор зависит от масштаба приложения, требований к отладке и опыту команды.
Мини-методология внедрения (шаги)
- Создать context/cartContext.js с CartContext и CartProvider.
- Определить initialState и cartReducer с набором экшенов.
- Реализовать addToCart/removeFromCart в провайдере и вернуть их в value.
- Обернуть приложение провайдером.
- Подключить контекст в компоненте продукта и реализовать UI-логику кнопки.
- Написать модульные тесты и интеграционные проверки (см. Критерии приёмки).
Критерии приёмки
- При добавлении товара он появляется в items провайдера.
- Поведение кнопки на странице продукта переключается между “Добавить в корзину” и “Удалить из корзины”.
- Удаление товара корректно исключает его по id.
- При перезагрузке страницы (если реализован localStorage) содержимое корзины восстанавливается.
Минимальные тесты (acceptance):
- Добавить товар → items.length увеличился на 1.
- Добавить тот же товар снова (если реализована quantity) → quantity увеличилась.
- Удалить товар → items не содержит элемент с этим id.
Чек-листы по ролям
Разработчик:
- Создал cartContext.js
- Реализовал редьюсер и экшены
- Подключил провайдер в корень приложения
- Написал компонент продукта с корректной подпиской на контекст
- Добавил тесты на add/remove
Код-ревьюер:
- Проверил иммутабельность обновлений state
- Убедился в отсутствии утечек при множественных dispatch
- Проверил обработку ошибок в редьюсере
Продакшн-операции:
- При необходимости добавлена синхронизация с localStorage
- Обработаны граничные случаи (пустой список, удаление несуществующего id)
Decision flow (Mermaid)
flowchart TD
A[Клик на кнопку] --> B{Товар в корзине?}
B -- Да --> C[Вызвать removeFromCart'id']
B -- Нет --> D[Вызвать addToCart'product']
C --> E[Обновить state через dispatch]
D --> E
E --> F[Обновить UI]Безопасность и приватность
- Не храните в клиентском стейте чувствительные данные пользователя.
- Если корзина синхронизируется с backend, используйте авторизацию и защиту от CSRF/IDOR.
Итог
Context API + useReducer дают простую, хорошо масштабируемую основу для корзины в Next.js. Подход хорош для большинства небольших и средних магазинов: он лёгок в отладке, читабелен и не требует внешних библиотек. По мере роста приложения можно постепенно переходить к более мощным менеджерам состояния или добавлять серверную синхронизацию.
Сводка:
- Начните с CartContext и простого редьюсера.
- Экспортируйте функции addToCart/removeFromCart через value провайдера.
- В компонентах пользуйтесь useContext и реагируйте на изменения items через useEffect.
Важно: адаптируйте структуру items (quantity, price, options) под требования вашего каталога и оформления заказа.
Похожие материалы
RDP: полный гид по настройке и безопасности
Android как клавиатура и трекпад для Windows
Советы и приёмы для работы с PDF
Calibration в Lightroom Classic: как и когда использовать
Отключить Siri Suggestions на iPhone