karbe/src/components/RentalCartProvider.tsx
Ubuntu 15f41a7e2a
All checks were successful
CI / test (pull_request) Successful in 2m21s
fix(rental): no setState in effect for cart hydration
ESLint react-hooks/set-state-in-effect bloque le CI. On déplace la
re-hydratation depuis le cookie dans le lazy initializer de useState
(qui ne court qu'une fois côté client). Conserve la cohérence si un
autre onglet a modifié le panier entre le render serveur et l'hydration,
sans déclencher de re-render en cascade.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-02 08:54:39 +00:00

110 lines
3 KiB
TypeScript

"use client";
import {
createContext,
useCallback,
useContext,
useMemo,
useState,
type ReactNode,
} from "react";
import {
CART_COOKIE,
EMPTY_CART,
parseCart,
serializeCart,
type Cart,
type CartEntry,
} from "@/lib/rental-cart";
type CartContextValue = {
cart: Cart;
addEntry: (entry: CartEntry) => void;
removeEntry: (index: number) => void;
updateEntry: (index: number, patch: Partial<CartEntry>) => void;
clear: () => void;
totalItems: number;
};
const Ctx = createContext<CartContextValue | null>(null);
function readCookieClient(): Cart {
if (typeof document === "undefined") return EMPTY_CART;
const match = document.cookie.split(/;\s*/).find((c) => c.startsWith(`${CART_COOKIE}=`));
if (!match) return EMPTY_CART;
const value = decodeURIComponent(match.slice(CART_COOKIE.length + 1));
return parseCart(value);
}
function writeCookieClient(cart: Cart): void {
if (typeof document === "undefined") return;
document.cookie = `${CART_COOKIE}=${encodeURIComponent(serializeCart(cart))}; path=/; max-age=${60 * 60 * 24 * 30}; SameSite=Lax`;
}
export function RentalCartProvider({ children, initial }: { children: ReactNode; initial?: Cart }) {
// Initial vient du serveur (cookie lu côté serveur). Sur le client on relit le
// cookie une seule fois via lazy initializer pour rester cohérent si un autre
// onglet a modifié le panier entre le render serveur et l'hydration.
const [cart, setCart] = useState<Cart>(() => {
if (typeof document === "undefined") return initial ?? EMPTY_CART;
const fromCookie = readCookieClient();
return fromCookie.items.length > 0 ? fromCookie : initial ?? EMPTY_CART;
});
const persist = useCallback((next: Cart) => {
setCart(next);
writeCookieClient(next);
}, []);
const addEntry = useCallback(
(entry: CartEntry) => {
const next = { ...cart, items: [...cart.items, entry] };
persist(next);
},
[cart, persist],
);
const removeEntry = useCallback(
(index: number) => {
const next = { ...cart, items: cart.items.filter((_, i) => i !== index) };
persist(next);
},
[cart, persist],
);
const updateEntry = useCallback(
(index: number, patch: Partial<CartEntry>) => {
const next = {
...cart,
items: cart.items.map((e, i) => (i === index ? { ...e, ...patch } : e)),
};
persist(next);
},
[cart, persist],
);
const clear = useCallback(() => {
persist({ v: 1, items: [] });
}, [persist]);
const value = useMemo<CartContextValue>(
() => ({
cart,
addEntry,
removeEntry,
updateEntry,
clear,
totalItems: cart.items.reduce((acc, e) => acc + e.qty, 0),
}),
[cart, addEntry, removeEntry, updateEntry, clear],
);
return <Ctx.Provider value={value}>{children}</Ctx.Provider>;
}
export function useCart(): CartContextValue {
const ctx = useContext(Ctx);
if (!ctx) throw new Error("useCart must be used inside <RentalCartProvider>");
return ctx;
}