Setting the file. One moment. Use Shop · Wix Vibe Headless · wix/skills · Skills Docs22.10
Post Detail
(opens in a new tab)
references/storefront/app/hooks/useShop.js
JavaScript·83 lines·4 KB
}
=
{}) {
9 const [categories, setCategories] = useState([]);
10 const [activeCategory, setActiveCategory] = useState(null);
11 const [sort, setSort] = useState("featured");
12 const [filters, setFilters] = useState({});
13 const [attempt, setAttempt] = useState(0);
14 const [page, setPage] = useState({ key: null, products: null, cursor: null, error: null, loadingMore: false });
15 const generation = useRef(0);
16 const pendingMore = useRef(null);
17 const key = JSON.stringify([pageSize, activeCategory?.id ?? null, sort, filters.minPrice ?? null,
18 filters.maxPrice ?? null, !!filters.inStockOnly, filters.search ?? "", attempt]);
19 const currentKey = useRef(key);
20 currentKey.current = key;
21
22 useEffect(() => {
23 let alive = true;
24 queryCategories({ limit: 100 })
25 .then(({ categories: list }) => {
26 if (alive) setCategories((list || []).filter(c => c.visible !== false && c.slug !== SYSTEM_CATEGORY_SLUG));
27 })
28 .catch(() => { if (alive) setCategories([]); });
29 return () => { alive = false; };
30 }, []);
31
32 useEffect(() => {
33 const id = ++generation.current;
34 let alive = true;
35 pendingMore.current = null;
36 setPage({ key, products: null, cursor: null, error: null, loadingMore: false });
37 const [limit, categoryId, selectedSort, minPrice, maxPrice, inStockOnly, search] = JSON.parse(key);
38 searchProducts({ limit, categoryId, sort: selectedSort, minPrice, maxPrice, inStockOnly, search })
39 .then(res => {
40 if (alive && generation.current === id && currentKey.current === key) {
41 setPage({ key, products: res.products, cursor: res.nextCursor, error: null, loadingMore: false });
42 }
43 })
44 .catch(e => {
45 if (alive && generation.current === id && currentKey.current === key) {
46 setPage({ key, products: [], cursor: null, error: e?.message || "Couldn't load products.", loadingMore: false });
47 }
48 });
49 return () => { alive = false; generation.current++; };
50 }, [key]);
51
52 const loadMore = useCallback(async () => {
53 if (page.key !== key || !page.cursor || pendingMore.current) return;
54 const request = { generation: generation.current, key };
55 pendingMore.current = request; // blocks repeated clicks before React rerenders
56 const isCurrent = () => generation.current === request.generation && currentKey.current === key;
57 setPage(p => ({ ...p, loadingMore: true, error: null }));
58 try {
59 const res = await searchProducts({ limit: pageSize, cursor: page.cursor });
60 if (isCurrent()) setPage(p => {
61 const seen = new Set((p.products || []).map(product => product.id));
62 return { ...p, products: [...(p.products || []), ...res.products.filter(product => {
63 if (seen.has(product.id)) return false;
64 seen.add(product.id); return true;
65 })], cursor: res.nextCursor };
66 });
67 } catch (e) {
68 if (isCurrent()) setPage(p => ({ ...p, error: e?.message || "Couldn't load more products." }));
69 } finally {
70 if (pendingMore.current === request) pendingMore.current = null;
71 if (isCurrent()) setPage(p => ({ ...p, loadingMore: false }));
72 }
73 }, [key, page.key, page.cursor, pageSize]);
74
75 const retry = useCallback(() => setAttempt(n => n + 1), []);
76 const current = page.key === key;
77 return {
78 categories, activeCategory, setActiveCategory, sort, setSort, filters, setFilters,
79 products: current ? page.products : null, loading: !current || page.products === null,
80 error: current ? page.error : null, retry,
81 hasMore: current && !!page.cursor, loadMore, loadingMore: current && page.loadingMore,
82 };
83}