Skip to content

Add memoization and performance optimizations for PLPs and PDPs - #33

Merged
damianlegawiec merged 5 commits into
mainfrom
performance-improvements
Feb 24, 2026
Merged

Add memoization and performance optimizations for PLPs and PDPs#33
damianlegawiec merged 5 commits into
mainfrom
performance-improvements

Conversation

@Cichorek

@Cichorek Cichorek commented Feb 24, 2026

Copy link
Copy Markdown
Contributor

Summary

Comprehensive memoization and caching improvements across PLPs (Product Listing Pages), PDPs (Product Detail Pages), and context providers to reduce unnecessary re-renders and improve rendering performance.

Closes #12

Changes (17 optimizations across 14 files)

Component memoization:

  • React.memo on ProductCard — prevents re-renders during infinite scroll, filter drawer toggle, loadingMore state changes
  • React.memo on ProductFilters — prevents re-renders when products/loading state changes (props stay stable)

Context provider optimization:

  • useMemo on StoreContext value + useCallback on setCountry/setLocale — stops cascade re-renders to ~20 consumers
  • useMemo on CartContext value + itemCount derived computation
  • useMemo on AuthContext value
  • useMemo on CheckoutContext value

PDP optimization:

  • Precomputed variant lookup maps in VariantPicker — O(1) lookups instead of O(n) per option value
  • MediaGallery image quality 100 → 85 for smaller payloads with imperceptible quality difference

PLP/useProductListing optimization:

  • filtersEqual() shallow comparison replacing JSON.stringify in handleFilterChange
  • useMemo on filterParamsKey instead of computing on every render
  • searchQueryRef to remove searchQuery from handleFilterChange deps
  • Memoized filterParams object in CategoryProductsContent
  • useMemo on listId/listName in both ProductsContent and CategoryProductsContent

GTM analytics fix:

  • prevLoadingRef pattern prevents duplicate view_item_list/view_search_results tracking on infinite scroll loadMore

UX improvements:

  • loading.tsx skeleton for /products route — instant visual feedback during navigation
  • loading.tsx skeleton for /t/[...permalink] category route — instant feedback while server fetches taxon data

Test plan

  • All 57 vitest tests pass
  • Biome lint/format check clean
  • Products page renders with filters sidebar
  • Category page renders with breadcrumbs, subcategories, and products
  • Zero console errors on fresh dev server
  • Loading skeletons display during route transitions

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added skeleton loading screens for product and category pages
  • Performance & Enhancements

    • Reduced unnecessary re-renders across product cards, filters, contexts, and listings
    • Faster and more responsive product variant selection
    • More accurate analytics triggering on fresh page loads
    • Updated main product image to use a blur placeholder and adjusted image quality settings

- React.memo on ProductCard and ProductFilters to prevent unnecessary re-renders
- useMemo on all context provider values (Store, Cart, Auth, Checkout) to stop cascade re-renders
- useCallback on StoreContext setters for stable function references
- Precomputed variant lookup maps in VariantPicker (O(1) vs O(n))
- Shallow filtersEqual() replacing JSON.stringify in useProductListing
- searchQueryRef + memoized filterParamsKey for stable callback deps
- prevLoadingRef pattern to prevent duplicate GTM tracking on infinite scroll
- useMemo on listId/listName in ProductsContent and CategoryProductsContent
- Memoized filterParams object in CategoryProductsContent
- MediaGallery image quality 100 → 85 for smaller payloads
- loading.tsx skeletons for /products and /t/[...permalink] routes
- Added .claude/launch.json to .gitignore

Closes #12

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Feb 24, 2026

Copy link
Copy Markdown
Contributor

Warning

Rate limit exceeded

@Cichorek has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 1 minutes and 41 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between 4436900 and 06e9187.

📒 Files selected for processing (3)
  • src/app/[country]/[locale]/(storefront)/t/[...permalink]/loading.tsx
  • src/components/products/ProductGridSkeleton.tsx
  • src/components/products/ProductListingLayout.tsx

Walkthrough

Adds memoization across hooks, contexts, and components; introduces skeleton loading components for product and category pages; optimizes VariantPicker and MediaGallery; refines analytics to fire only on fresh listing loads; minor .gitignore addition.

Changes

Cohort / File(s) Summary
Loading UI Components
src/app/[country]/[locale]/(storefront)/products/loading.tsx, src/app/[country]/[locale]/(storefront)/t/[...permalink]/loading.tsx
Adds ProductsLoading and CategoryLoading skeleton components for responsive loading UIs.
Page Content & Analytics
src/app/[country]/[locale]/(storefront)/products/ProductsContent.tsx, src/app/[country]/[locale]/(storefront)/t/[...permalink]/CategoryProductsContent.tsx
Memoizes listId/listName and filter params; adds prevLoadingRef to trigger analytics only on transitions from loading→not-loading; updates effect dependencies.
Hook: Product Listing
src/hooks/useProductListing.ts
Replaces JSON.stringify comparisons with shallow filtersEqual, adds refs and memoization for stable filter/searchQuery handling and load control.
Variant & Media
src/components/products/VariantPicker.tsx, src/components/products/MediaGallery.tsx
VariantPicker: precomputes lookup maps via useMemo for O(1) checks. MediaGallery: adds base64 blur placeholder and reduces main image quality to 85.
Component Memoization
src/components/products/ProductCard.tsx, src/components/products/ProductFilters.tsx
Wraps components with React.memo to reduce unnecessary re-renders.
Context Value Stabilization
src/contexts/AuthContext.tsx, src/contexts/CartContext.tsx, src/contexts/CheckoutContext.tsx, src/contexts/StoreContext.tsx
Uses useMemo (and useCallback for setters) to provide stable provider values and reduce consumer re-renders.
Configuration
.gitignore
Adds an ignore rule for .claude/.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Client
    participant Hook as useProductListing
    participant Page as ProductsContent / CategoryProductsContent
    participant Analytics as AnalyticsService

    Client->>Hook: request products (filters / query)
    Hook-->>Page: update listing (loading, products, totalCount)
    Page->>Page: compare prevLoadingRef and listing.loading
    alt transition: loading -> not loading and products present
        Page->>Analytics: trackViewItemList / trackViewSearchResults (listId/listName/query)
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • damianlegawiec

Poem

🐇 I hop through props and cache the pace,
I paint soft skeletons while pages race,
I only call analytics when loads are new,
A blur, fewer renders, and a happier view. ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 13.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: comprehensive memoization and performance optimizations applied to PLPs, PDPs, and multiple context providers.
Linked Issues check ✅ Passed All changes directly address issue #12 objectives: memoization broadly implemented, unnecessary renders reduced across components/contexts, and targeted optimizations applied to PLPs/PDPs.
Out of Scope Changes check ✅ Passed All changes are in-scope: memoization optimizations, context providers, component memoization, analytics tracking fixes, and loading skeletons align with issue #12 performance improvement objectives.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch performance-improvements

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/components/products/MediaGallery.tsx (1)

42-49: ⚠️ Potential issue | 🟡 Minor

Add placeholder and blurDataURL to the Image component for optimal loading UX.

The main Image uses priority for performance but is missing placeholder and blurDataURL, which are required by the coding guidelines for Image optimization in this component directory. The thumbnail and lightbox Images should also include these props for consistency.

✅ Suggested updates

Main image (lines 42-50):

           <Image
             src={mainImageUrl}
             alt={selectedImage?.alt || productName}
             fill
             className="object-cover"
             priority
             quality={85}
+            placeholder="blur"
+            blurDataURL="data:image/gif;base64,R0lGODlhAQABAAAAACw="
             sizes="(max-width: 768px) 100vw, 50vw"
           />

Thumbnail images and lightbox image should also add these props.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/products/MediaGallery.tsx` around lines 42 - 49, The Image
components (the main Image that uses mainImageUrl and alt={selectedImage?.alt ||
productName}, as well as the thumbnail and lightbox Image instances) must
include placeholder="blur" and a blurDataURL prop; update the main Image JSX to
add placeholder="blur" and blurDataURL={selectedImage?.blurDataURL ||
someFallbackBlur} (or derive from mainImageUrl), and apply the same pattern to
the thumbnail rendering and the lightbox Image so all Image usages provide a
blurDataURL fallback and placeholder="blur" for consistent optimized loading UX.
src/components/products/VariantPicker.tsx (1)

78-100: ⚠️ Potential issue | 🟡 Minor

Potential inconsistency: findVariant uses exact count, availability checks use subset matching.

findVariant (line 82) enforces variant.option_values.length === optionCount, so it only accepts a variant whose total option count exactly matches newOptions. By contrast, isOptionAvailable and isOptionPurchasable (lines 96-100, 109-115) use subset matching — a variant with more options than testOptions is considered a valid match.

The consequence: for any variant with extra option types not yet present in selectedOptions, the option will appear available/purchasable, but clicking it calls onVariantChange(null) because findVariant rejects the variant.

In standard Spree catalogs (all variants share the same option types) this is harmless. If progressive partial-selection is intentional, consider either:

  • documenting the expected behavior explicitly, or
  • making the three functions consistent by adding the same length guard to the availability checks:
🔧 Proposed consistency fix
  const isOptionAvailable = (
    optionTypeId: string,
    optionValue: string,
  ): boolean => {
    const testOptions = { ...selectedOptions, [optionTypeId]: optionValue };
+   const testCount = Object.keys(testOptions).length;
    return variantOptionMaps.some(({ optionsMap }) =>
+     Object.keys(optionsMap).length === testCount &&
      Object.entries(testOptions).every(
        ([typeId, value]) => optionsMap[typeId] === value,
      ),
    );
  };

  const isOptionPurchasable = (
    optionTypeId: string,
    optionValue: string,
  ): boolean => {
    const testOptions = { ...selectedOptions, [optionTypeId]: optionValue };
+   const testCount = Object.keys(testOptions).length;
    return variantOptionMaps.some(
      ({ variant, optionsMap }) =>
        variant.purchasable &&
+       Object.keys(optionsMap).length === testCount &&
        Object.entries(testOptions).every(
          ([typeId, value]) => optionsMap[typeId] === value,
        ),
    );
  };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/products/VariantPicker.tsx` around lines 78 - 100, findVariant
enforces exact-option-count matching while isOptionAvailable and
isOptionPurchasable use subset matching, causing clicks to yield null; make the
behavior consistent by adding the same length guard to the
availability/purchasability checks: in isOptionAvailable and
isOptionPurchasable, when iterating variantOptionMaps check both that
optionsMap's key count equals Object.keys(testOptions).length (or
Object.keys(newOptions).length) and that every ([typeId, value]) in testOptions
matches optionsMap[typeId]; reference the functions findVariant,
isOptionAvailable, isOptionPurchasable, variantOptionMaps, selectedOptions and
newOptions when applying this change.
🧹 Nitpick comments (7)
.gitignore (1)

44-45: Consider ignoring the entire .claude/ directory instead of a single file.

Claude Code can generate additional local artefacts beyond launch.json (e.g., settings files, history). Scoping the ignore to just the one file risks accidentally committing other IDE-generated content.

♻️ Proposed change
 # claude code
-.claude/launch.json
+.claude/
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.gitignore around lines 44 - 45, Replace the specific `.claude/launch.json`
entry with a broader `.claude/` directory ignore in the .gitignore so all Claude
Code-generated artifacts (launch.json, settings, history, etc.) are excluded;
update the existing `.claude/launch.json` line to `.claude/` (or add `.claude/`
and remove the single-file entry) to ensure no other IDE-generated files are
accidentally committed.
src/contexts/AuthContext.tsx (1)

116-129: Add an explicit return type for the memoized context value.

♻️ Suggested change
-  const value = useMemo(
+  const value = useMemo<AuthContextType>(
     () => ({
       user,
       loading,
       login,
       register,
       logout,
       refreshUser,
       isAuthenticated: !!user,
     }),
     [user, loading, login, register, logout, refreshUser],
   );

As per coding guidelines: "Use strict TypeScript type checking; define explicit return types for functions and avoid 'any' type".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/contexts/AuthContext.tsx` around lines 116 - 129, The memoized context
value assigned to value needs an explicit TypeScript type: declare or reuse an
AuthContextValue interface/type that describes { user, loading, login, register,
logout, refreshUser, isAuthenticated:boolean } and apply it to the memo result
(e.g., annotate useMemo with the AuthContextValue generic or set const value:
AuthContextValue = useMemo(...)). Update the value declaration used by
AuthContext.Provider and ensure the type matches the AuthContext's expected type
so strict TS checks pass.
src/contexts/CheckoutContext.tsx (1)

23-30: Add an explicit return type for the memoized context value.

Prefer a typed useMemo to satisfy explicit return type guidance.

♻️ Suggested change
-  const value = useMemo(
-    () => ({ summaryContent, setSummaryContent }),
-    [summaryContent],
-  );
+  const value = useMemo<CheckoutContextValue>(
+    () => ({ summaryContent, setSummaryContent }),
+    [summaryContent],
+  );

As per coding guidelines: "Use strict TypeScript type checking; define explicit return types for functions and avoid 'any' type".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/contexts/CheckoutContext.tsx` around lines 23 - 30, The memoized context
value created with useMemo (the variable value used in CheckoutContext.Provider)
lacks an explicit return type; update the useMemo call (or explicitly type the
value variable) to a concrete interface describing { summaryContent: typeof
summaryContent; setSummaryContent: typeof setSummaryContent } so TypeScript
infers a strict type instead of any—i.e., declare a ContextValue type and apply
it as the generic to useMemo (or annotate value: ContextValue) when creating
value for CheckoutContext.Provider.
src/contexts/CartContext.tsx (1)

126-164: Type the memoized values explicitly.

This aligns the new memoized computations with the explicit return type guideline.

♻️ Suggested change
-  const itemCount = useMemo(
+  const itemCount = useMemo<number>(
     () =>
       cart?.line_items?.reduce(
         (sum: number, item: StoreLineItem) => sum + item.quantity,
         0,
       ) ?? 0,
     [cart],
   );

-  const value = useMemo(
+  const value = useMemo<CartContextType>(
     () => ({
       cart,
       loading,
       updating,
       itemCount,
       isOpen,
       openCart,
       closeCart,
       addItem,
       updateItem,
       removeItem,
       refreshCart,
     }),
     [
       cart,
       loading,
       updating,
       itemCount,
       isOpen,
       openCart,
       closeCart,
       addItem,
       updateItem,
       removeItem,
       refreshCart,
     ],
   );

As per coding guidelines: "Use strict TypeScript type checking; define explicit return types for functions and avoid 'any' type".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/contexts/CartContext.tsx` around lines 126 - 164, The two useMemo calls
need explicit TypeScript return types: annotate itemCount as number (e.g.,
useMemo<number>(...)) and annotate value with the Cart context type (e.g.,
useMemo<CartContextType>(...)) so the memoized values match the project's strict
typing rules; locate the itemCount and value useMemo calls and add the
appropriate generic/type annotation (import or reference the existing
CartContextType or the exported type of CartContext) without changing the
implementation or dependency arrays.
src/contexts/StoreContext.tsx (1)

152-191: Type the callbacks and memoized context value explicitly.

♻️ Suggested change
-  const setCountry = useCallback(
+  const setCountry = useCallback<StoreContextValue["setCountry"]>(
     (newCountry: string) => {
       setCountryState(newCountry);
       const countryObj = findCountry(countries, newCountry);
       if (countryObj?.currency) {
         setCurrency(countryObj.currency);
       }
     },
     [countries],
   );

-  const setLocale = useCallback((newLocale: string) => {
+  const setLocale = useCallback<StoreContextValue["setLocale"]>((newLocale) => {
     setLocaleState(newLocale);
   }, []);

-  const value = useMemo(
+  const value = useMemo<StoreContextValue>(
     () => ({
       country,
       locale,
       currency,
       store,
       countries,
       setCountry,
       setLocale,
       loading,
     }),
     [
       country,
       locale,
       currency,
       store,
       countries,
       setCountry,
       setLocale,
       loading,
     ],
   );

As per coding guidelines: "Use strict TypeScript type checking; define explicit return types for functions and avoid 'any' type".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/contexts/StoreContext.tsx` around lines 152 - 191, Annotate the callbacks
and the memoized context value with explicit TypeScript types: give setCountry a
typed parameter and return type (e.g., (newCountry: string) => void) and
setLocale a typed signature (e.g., (newLocale: string) => void), and type the
useMemo result with the StoreContext value interface (the context value type
used by StoreContext) instead of inferring any; update the variable names
setCountry, setLocale and value to use those explicit types so the Provider
receives a correctly typed value object (country, locale, currency, store,
countries, setCountry, setLocale, loading).
src/hooks/useProductListing.ts (2)

14-25: Normalize optionValues comparison if order isn’t guaranteed.

If optionValues is treated as a set, the current order-sensitive compare can trigger redundant reloads. Consider normalizing before compare (only if ordering has no semantic meaning).

♻️ Optional normalization
 function filtersEqual(a: ActiveFilters, b: ActiveFilters): boolean {
   if (a === b) return true;
   if (a.priceMin !== b.priceMin || a.priceMax !== b.priceMax) return false;
   if (a.availability !== b.availability) return false;
   if (a.sortBy !== b.sortBy) return false;
-  if (a.optionValues.length !== b.optionValues.length) return false;
-  for (let i = 0; i < a.optionValues.length; i++) {
-    if (a.optionValues[i] !== b.optionValues[i]) return false;
-  }
+  const aOptions = [...a.optionValues].sort();
+  const bOptions = [...b.optionValues].sort();
+  if (aOptions.length !== bOptions.length) return false;
+  for (let i = 0; i < aOptions.length; i++) {
+    if (aOptions[i] !== bOptions[i]) return false;
+  }
   return true;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/hooks/useProductListing.ts` around lines 14 - 25, The filtersEqual
function currently compares optionValues order-sensitively which can cause
unnecessary reloads; update filtersEqual to normalize optionValues (e.g., sort
a.optionValues and b.optionValues or compare as Sets) before comparing so
order-independent equality is used when ordering has no semantic meaning;
preserve existing checks for priceMin, priceMax, availability, and sortBy and
ensure normalization is done on copies to avoid mutating the original
ActiveFilters.

8-8: Stabilize filterParamsKey against key-order churn.

JSON.stringify on objects is sensitive to insertion order and may cause unnecessary refetches if filterParams is created with non-deterministic key order. Consider normalizing key order before stringifying.

♻️ Suggested normalization
-  const filterParamsKey = useMemo(
-    () => JSON.stringify(filterParams),
-    [filterParams],
-  );
+  const filterParamsKey = useMemo(
+    () =>
+      JSON.stringify(
+        Object.fromEntries(
+          Object.entries(filterParams).sort(([a], [b]) =>
+            a.localeCompare(b),
+          ),
+        ),
+      ),
+    [filterParams],
+  );

Also applies to: 65-71

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/hooks/useProductListing.ts` at line 8, filterParamsKey currently uses
JSON.stringify(filterParams) which is unstable across object key insertion
orders and causes spurious refetches; replace that with a deterministic
serialization by normalizing filterParams before stringifying (e.g., implement a
stableSortKeys function or stableStringify that sorts object keys recursively)
and use that result wherever filterParamsKey is computed (reference symbol:
filterParamsKey and the variable filterParams in useProductListing, also update
the other usages around the block referenced by lines 65-71). Ensure the
normalization handles nested objects and arrays so the produced string is stable
across key-order churn.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/products/VariantPicker.tsx`:
- Around line 124-130: Add an explicit return type to the getOptionValueDetails
function: change its signature to declare it returns
StoreVariant["option_values"][0] | null (this matches the values stored in
optionValueDetailsMap). Update the function declaration for
getOptionValueDetails(optionTypeId: string, optionValueName: string) to include
the explicit return type StoreVariant["option_values"][0] | null and keep the
existing body that returns
optionValueDetailsMap[`${optionTypeId}:${optionValueName}`] || null.

---

Outside diff comments:
In `@src/components/products/MediaGallery.tsx`:
- Around line 42-49: The Image components (the main Image that uses mainImageUrl
and alt={selectedImage?.alt || productName}, as well as the thumbnail and
lightbox Image instances) must include placeholder="blur" and a blurDataURL
prop; update the main Image JSX to add placeholder="blur" and
blurDataURL={selectedImage?.blurDataURL || someFallbackBlur} (or derive from
mainImageUrl), and apply the same pattern to the thumbnail rendering and the
lightbox Image so all Image usages provide a blurDataURL fallback and
placeholder="blur" for consistent optimized loading UX.

In `@src/components/products/VariantPicker.tsx`:
- Around line 78-100: findVariant enforces exact-option-count matching while
isOptionAvailable and isOptionPurchasable use subset matching, causing clicks to
yield null; make the behavior consistent by adding the same length guard to the
availability/purchasability checks: in isOptionAvailable and
isOptionPurchasable, when iterating variantOptionMaps check both that
optionsMap's key count equals Object.keys(testOptions).length (or
Object.keys(newOptions).length) and that every ([typeId, value]) in testOptions
matches optionsMap[typeId]; reference the functions findVariant,
isOptionAvailable, isOptionPurchasable, variantOptionMaps, selectedOptions and
newOptions when applying this change.

---

Nitpick comments:
In @.gitignore:
- Around line 44-45: Replace the specific `.claude/launch.json` entry with a
broader `.claude/` directory ignore in the .gitignore so all Claude
Code-generated artifacts (launch.json, settings, history, etc.) are excluded;
update the existing `.claude/launch.json` line to `.claude/` (or add `.claude/`
and remove the single-file entry) to ensure no other IDE-generated files are
accidentally committed.

In `@src/contexts/AuthContext.tsx`:
- Around line 116-129: The memoized context value assigned to value needs an
explicit TypeScript type: declare or reuse an AuthContextValue interface/type
that describes { user, loading, login, register, logout, refreshUser,
isAuthenticated:boolean } and apply it to the memo result (e.g., annotate
useMemo with the AuthContextValue generic or set const value: AuthContextValue =
useMemo(...)). Update the value declaration used by AuthContext.Provider and
ensure the type matches the AuthContext's expected type so strict TS checks
pass.

In `@src/contexts/CartContext.tsx`:
- Around line 126-164: The two useMemo calls need explicit TypeScript return
types: annotate itemCount as number (e.g., useMemo<number>(...)) and annotate
value with the Cart context type (e.g., useMemo<CartContextType>(...)) so the
memoized values match the project's strict typing rules; locate the itemCount
and value useMemo calls and add the appropriate generic/type annotation (import
or reference the existing CartContextType or the exported type of CartContext)
without changing the implementation or dependency arrays.

In `@src/contexts/CheckoutContext.tsx`:
- Around line 23-30: The memoized context value created with useMemo (the
variable value used in CheckoutContext.Provider) lacks an explicit return type;
update the useMemo call (or explicitly type the value variable) to a concrete
interface describing { summaryContent: typeof summaryContent; setSummaryContent:
typeof setSummaryContent } so TypeScript infers a strict type instead of
any—i.e., declare a ContextValue type and apply it as the generic to useMemo (or
annotate value: ContextValue) when creating value for CheckoutContext.Provider.

In `@src/contexts/StoreContext.tsx`:
- Around line 152-191: Annotate the callbacks and the memoized context value
with explicit TypeScript types: give setCountry a typed parameter and return
type (e.g., (newCountry: string) => void) and setLocale a typed signature (e.g.,
(newLocale: string) => void), and type the useMemo result with the StoreContext
value interface (the context value type used by StoreContext) instead of
inferring any; update the variable names setCountry, setLocale and value to use
those explicit types so the Provider receives a correctly typed value object
(country, locale, currency, store, countries, setCountry, setLocale, loading).

In `@src/hooks/useProductListing.ts`:
- Around line 14-25: The filtersEqual function currently compares optionValues
order-sensitively which can cause unnecessary reloads; update filtersEqual to
normalize optionValues (e.g., sort a.optionValues and b.optionValues or compare
as Sets) before comparing so order-independent equality is used when ordering
has no semantic meaning; preserve existing checks for priceMin, priceMax,
availability, and sortBy and ensure normalization is done on copies to avoid
mutating the original ActiveFilters.
- Line 8: filterParamsKey currently uses JSON.stringify(filterParams) which is
unstable across object key insertion orders and causes spurious refetches;
replace that with a deterministic serialization by normalizing filterParams
before stringifying (e.g., implement a stableSortKeys function or
stableStringify that sorts object keys recursively) and use that result wherever
filterParamsKey is computed (reference symbol: filterParamsKey and the variable
filterParams in useProductListing, also update the other usages around the block
referenced by lines 65-71). Ensure the normalization handles nested objects and
arrays so the produced string is stable across key-order churn.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a5532e2 and 45819fa.

📒 Files selected for processing (14)
  • .gitignore
  • src/app/[country]/[locale]/(storefront)/products/ProductsContent.tsx
  • src/app/[country]/[locale]/(storefront)/products/loading.tsx
  • src/app/[country]/[locale]/(storefront)/t/[...permalink]/CategoryProductsContent.tsx
  • src/app/[country]/[locale]/(storefront)/t/[...permalink]/loading.tsx
  • src/components/products/MediaGallery.tsx
  • src/components/products/ProductCard.tsx
  • src/components/products/ProductFilters.tsx
  • src/components/products/VariantPicker.tsx
  • src/contexts/AuthContext.tsx
  • src/contexts/CartContext.tsx
  • src/contexts/CheckoutContext.tsx
  • src/contexts/StoreContext.tsx
  • src/hooks/useProductListing.ts

Comment thread src/components/products/VariantPicker.tsx
…comparison

- Add explicit return type to VariantPicker getOptionValueDetails
- Add placeholder="blur" with static base64 fallback to MediaGallery main image
- Broaden .gitignore from .claude/launch.json to .claude/
- Add explicit useMemo generics on all 4 context providers (Auth, Cart, Checkout, Store)
- Make filtersEqual optionValues comparison order-independent via sorted copies

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
src/hooks/useProductListing.ts (1)

114-118: Consider moving state‑driven data fetching out of useEffect.

Both effects kick off network calls when state/props change. If feasible, trigger these fetches from explicit event handlers or Server Actions and reserve useEffect for external synchronization.
As per coding guidelines: Avoid using useEffect for data fetching triggered by state changes; use event handlers or Server Actions instead.

Also applies to: 152-156

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/hooks/useProductListing.ts` around lines 114 - 118, The useEffect blocks
in useProductListing.ts that currently trigger network calls based on state (the
effect watching storeLoading and filterParamsKey, and the other effect around
lines 152-156) should be refactored to move state-driven fetching into explicit
handlers or Server Actions: extract the fetch logic into named functions (e.g.,
fetchFilters and fetchProducts), remove the network calls from the useEffect
bodies that reference storeLoading and filterParamsKey, and invoke those fetch
functions from explicit event handlers or expose them as Server Actions so
callers can trigger them when state changes; keep useEffect only for
synchronizing external listeners (e.g., subscribe/unsubscribe) and ensure
filterParamsKey and storeLoading are no longer directly causing fetches inside
useEffect.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/contexts/StoreContext.tsx`:
- Around line 152-166: Add explicit void return types to the two callback
declarations: annotate the useCallback handlers for setCountry and setLocale
with ": void" (i.e., change the function signatures for setCountry and setLocale
to return void) so TypeScript strict checking is satisfied; update the
declarations for the setCountry callback (which calls setCountryState,
findCountry, setCurrency) and the setLocale callback (which calls
setLocaleState) to include the explicit return type.

In `@src/hooks/useProductListing.ts`:
- Around line 15-16: The short-circuit in filtersEqual (function filtersEqual(a:
ActiveFilters, b: ActiveFilters)) returns true for reference-equality which
hides in-place mutations; remove the "if (a === b) return true;" line and
instead always perform the structural comparison of ActiveFilters fields (or
replace the function with a deep/structural equality check) so callers who
mutate the same object instance are detected; update any tests if they relied on
the old behavior.

---

Nitpick comments:
In `@src/hooks/useProductListing.ts`:
- Around line 114-118: The useEffect blocks in useProductListing.ts that
currently trigger network calls based on state (the effect watching storeLoading
and filterParamsKey, and the other effect around lines 152-156) should be
refactored to move state-driven fetching into explicit handlers or Server
Actions: extract the fetch logic into named functions (e.g., fetchFilters and
fetchProducts), remove the network calls from the useEffect bodies that
reference storeLoading and filterParamsKey, and invoke those fetch functions
from explicit event handlers or expose them as Server Actions so callers can
trigger them when state changes; keep useEffect only for synchronizing external
listeners (e.g., subscribe/unsubscribe) and ensure filterParamsKey and
storeLoading are no longer directly causing fetches inside useEffect.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 45819fa and a27927b.

📒 Files selected for processing (8)
  • .gitignore
  • src/components/products/MediaGallery.tsx
  • src/components/products/VariantPicker.tsx
  • src/contexts/AuthContext.tsx
  • src/contexts/CartContext.tsx
  • src/contexts/CheckoutContext.tsx
  • src/contexts/StoreContext.tsx
  • src/hooks/useProductListing.ts
🚧 Files skipped from review as they are similar to previous changes (5)
  • src/components/products/MediaGallery.tsx
  • src/contexts/CheckoutContext.tsx
  • .gitignore
  • src/components/products/VariantPicker.tsx
  • src/contexts/AuthContext.tsx

Comment thread src/contexts/StoreContext.tsx
Comment thread src/hooks/useProductListing.ts Outdated
- Add explicit `: void` return types to setCountry/setLocale callbacks
  in StoreContext for type-safety consistency
- Remove reference-equality short-circuit from filtersEqual to prevent
  hiding in-place mutations on the ActiveFilters ref

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (2)
src/contexts/StoreContext.tsx (1)

152-165: useCallback deps and return types look correct.

setCountry depends on countries (used in findCountry), and setLocale deps [] is correct since setLocaleState is a stable setter. The explicit : void return types resolve the prior review round's feedback.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/contexts/StoreContext.tsx` around lines 152 - 165, Both callbacks are
correctly typed and their dependency arrays are appropriate: keep setCountry as
the useCallback that depends on countries and uses findCountry to set currency
(function name: setCountry, helper: findCountry, state setters: setCountryState,
setCurrency), and keep setLocale as a no-deps callback that calls setLocaleState
(function name: setLocale, state setter: setLocaleState); no code changes
required.
src/hooks/useProductListing.ts (1)

14-26: filtersEqual implementation is correct; prior reference-equality concern is addressed.

All five ActiveFilters fields are compared, optionValues uses a non-mutating sorted comparison for set equality, and the if (a === b) short-circuit that could mask in-place mutations is absent.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/hooks/useProductListing.ts` around lines 14 - 26, The filtersEqual
function correctly performs a shallow comparison of ActiveFilters (checking
priceMin, priceMax, availability, sortBy and set-equality of optionValues via
non-mutating sorted arrays) and does not use the problematic a === b
short-circuit; no change is required—leave the filtersEqual implementation as-is
in useProductListing.ts.
🧹 Nitpick comments (1)
src/hooks/useProductListing.ts (1)

114-157: void filterParamsKey; can be replaced with a comment.

void expr; as a dep-tracking idiom suppresses no-unused-expressions lint but looks odd. Since react-hooks/exhaustive-deps does not require a variable in the deps array to appear in the effect body, void filterParamsKey; can be dropped and the intent documented with a comment instead.

♻️ Proposed cleanup (apply to both occurrences)
   useEffect(() => {
     if (storeLoading) return;
-    // Track filterParams changes for re-fetching on soft-nav
-    void filterParamsKey;
+    // filterParamsKey in the deps array triggers a re-fetch when filterParams change (soft-nav)

     let cancelled = false;
   useEffect(() => {
     if (storeLoading) return;
-    // Track filterParams changes for re-fetching on soft-nav
-    void filterParamsKey;
+    // filterParamsKey in the deps array triggers a re-fetch when filterParams change (soft-nav)
     loadProducts(filtersRef.current, searchQuery);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/hooks/useProductListing.ts` around lines 114 - 157, Remove the odd "void
filterParamsKey;" statements inside the two useEffect blocks and replace them
with a short explanatory comment stating that filterParamsKey is intentionally
included in the dependency array for re-fetching on soft-nav; update the two
places in useProductListing.ts where useEffect (the fetchFilters effect and the
loadProducts effect) references filterParamsKey so the effect body no longer
contains the unused expression but keeps filterParamsKey in the dependency array
to maintain the intended behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/contexts/StoreContext.tsx`:
- Around line 152-165: Both callbacks are correctly typed and their dependency
arrays are appropriate: keep setCountry as the useCallback that depends on
countries and uses findCountry to set currency (function name: setCountry,
helper: findCountry, state setters: setCountryState, setCurrency), and keep
setLocale as a no-deps callback that calls setLocaleState (function name:
setLocale, state setter: setLocaleState); no code changes required.

In `@src/hooks/useProductListing.ts`:
- Around line 14-26: The filtersEqual function correctly performs a shallow
comparison of ActiveFilters (checking priceMin, priceMax, availability, sortBy
and set-equality of optionValues via non-mutating sorted arrays) and does not
use the problematic a === b short-circuit; no change is required—leave the
filtersEqual implementation as-is in useProductListing.ts.

---

Nitpick comments:
In `@src/hooks/useProductListing.ts`:
- Around line 114-157: Remove the odd "void filterParamsKey;" statements inside
the two useEffect blocks and replace them with a short explanatory comment
stating that filterParamsKey is intentionally included in the dependency array
for re-fetching on soft-nav; update the two places in useProductListing.ts where
useEffect (the fetchFilters effect and the loadProducts effect) references
filterParamsKey so the effect body no longer contains the unused expression but
keeps filterParamsKey in the dependency array to maintain the intended behavior.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a27927b and 4436900.

📒 Files selected for processing (2)
  • src/contexts/StoreContext.tsx
  • src/hooks/useProductListing.ts

Cichorek and others added 2 commits February 24, 2026 13:16
The same 6-item skeleton grid was duplicated in three places:
loading.tsx (products), loading.tsx (category), and ProductListingLayout.
Extract into a reusable component that mirrors the actual ProductCard
structure (rounded card container, padded content area, proper sizing).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The products page.tsx has no server-side data fetching (only
await params), so the loading.tsx Suspense fallback never shows.
Client-side loading is already handled by ProductListingLayout
via ProductGridSkeleton.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@damianlegawiec
damianlegawiec merged commit 8698cae into main Feb 24, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Performance improvements

2 participants