27. Реальный проект: E-commerce
Собираем всё изученное вместе в полноценный e-commerce интерфейс. Каталог товаров с фильтрами, поиском, корзиной с оптимистичными обновлениями и имитацией loader/action паттернов Remix.
Архитектура проекта
Заголовок раздела «Архитектура проекта»app/├── routes/│ ├── _store.tsx # Общий layout магазина│ ├── _store._index.tsx # Главная / каталог│ ├── _store.products.$id.tsx # Страница товара│ ├── _store.cart.tsx # Корзина│ └── api.cart.ts # Resource Route для корзины├── models/│ ├── product.server.ts # Работа с товарами│ └── cart.server.ts # Работа с корзиной└── session.server.ts # Сессии для корзиныloader для каталога
Заголовок раздела «loader для каталога»export async function loader({ request }) { const url = new URL(request.url); const category = url.searchParams.get("category"); const q = url.searchParams.get("q"); const sort = url.searchParams.get("sort") ?? "popular";
const [products, categories] = await Promise.all([ getProducts({ category, q, sort }), getCategories(), ]);
return json({ products, categories, q, category, sort });}action для корзины
Заголовок раздела «action для корзины»// app/routes/api.cart.ts (Resource Route)export async function action({ request }) { const session = await getSession(request.headers.get("Cookie")); const formData = await request.formData(); const intent = formData.get("intent") as string;
switch (intent) { case "add": addToCart(session, formData.get("productId") as string); break; case "remove": removeFromCart(session, formData.get("productId") as string); break; case "clear": clearCart(session); break; }
return json({ ok: true }, { headers: { "Set-Cookie": await commitSession(session) }, });}Оптимистичная корзина
Заголовок раздела «Оптимистичная корзина»function AddToCartButton({ product }) { const fetcher = useFetcher();
// Оптимистично определяем состояние const isAdding = fetcher.state !== "idle" && fetcher.formData?.get("intent") === "add";
return ( <fetcher.Form method="post" action="/api/cart"> <input type="hidden" name="intent" value="add" /> <input type="hidden" name="productId" value={product.id} /> <button type="submit" disabled={isAdding}> {isAdding ? "Добавляется..." : "В корзину"} </button> </fetcher.Form> );}