Skip to content

Conversation

@mkucmus
Copy link
Contributor

@mkucmus mkucmus commented Oct 3, 2025

Refactor useListing composable for better modularity and SSR support

closes: #841
blocked by: #2030

Areas covered:

  • cms-base-layer
  • composables

Overview

Refactored the monolithic useListing composable into smaller, focused modules with improved SSR support and early data access for product listings (to avoid ClientOnly loading without hydration mismatch).

Changes

1. Modular Composable Architecture

Split useListing.ts into focused, single-responsibility composables:

  • useListingCore.ts - Core state management with provide/inject pattern

    • Manages loading states, initial/applied listings
    • Provides shared state via unique listingKey
    • Includes dev-mode warnings for duplicate contexts
  • useProductListingProducts.ts - Product data and search operations

    • Products list, total count
    • search(), loadMore(), setInitialListing()
    • Loading states
  • useProductListingPagination.ts - Pagination logic

    • Current page, total pages, limit
    • changeCurrentPage()
  • useProductListingSorting.ts - Sorting functionality

    • Available sort orders
    • Current sorting order
    • changeCurrentSortingOrder()
  • useProductListingFilters.ts - Filter management

    • Available/current filters
    • setCurrentFilters(), resetFilters(), filtersToQuery()
  • utils.ts - Shared types and utilities

    • Moved merge(), isObject() helpers
    • Exported all TypeScript types for reuse

2. SSR Optimization

Early data access pattern:

  • getProductListingFromCmsPage.ts helper in @shopware/helpers

    • Extracts product listing from CMS page structure
    • Optimized algorithm with early exits and minimal allocations
    • Returns null if no listing found
  • CmsPage.vue - Extract listing on component setup

  const initialListing = getProductListingFromCmsPage(props.content);
  if (initialListing) {
    createCategoryListingContext(initialListing);
  }

listing-related composables dependency graph

graph TD
    A[CmsPage.vue] -->|1. Extract listing from CMS| B[getProductListingFromCmsPage helper]
    B -->|2. Returns ProductListingResult or null| A
    A -->|3. Pass initialListing| C[createCategoryListingContext]
    
    C -->|4. Creates context with initialListing param| D[useListing composable]
    
    D -->|5. Pass initialListing| E[useListingCore]
    E -->|Provides shared state via inject/provide| F[Core State Management]
    
    F -.->|Injects core| G[useProductListingProducts]
    F -.->|Injects core| H[useProductListingPagination]
    F -.->|Injects core| I[useProductListingSorting]
    F -.->|Injects core| J[useProductListingFilters]
    
    D -->|6. Composes all modules| K[Facade Pattern useListing]
    K -->|Returns unified API| L[Components]
    
    L -->|Use listing composables| M[SwProductListingFilters.vue]
    L -->|Use listing composables| N[CmsElementProductListing.vue]
    L -->|Use listing composables| O[Search.vue]
    
    O -->|Manual setInitialListing call| G
    
    G -->|products, loading, search, loadMore| L
    H -->|pagination methods| L
    I -->|sorting methods| L
    J -->|filters methods| L
    
    P[utils.ts] -.->|Shared types & helpers| D
    P -.->|merge, isObject| E
    
    Q[Filter Components] -->|Use filter data| M
    Q1[SwFilterProperties.vue] -.-> Q
    Q2[SwFilterRating.vue] -.-> Q
    Q3[SwFilterPrice.vue] -.-> Q
    Q4[SwFilterShippingFree.vue] -.-> Q
    
    style A fill:#e1f5ff
    style E fill:#fff3e0
    style D fill:#f3e5f5
    style K fill:#e8f5e9
    style L fill:#fce4ec
    
    subgraph "SSR Flow (Category Pages)"
    A
    B
    C
    end
    
    subgraph "Core State Management"
    E
    F
    end
    
    subgraph "Modular Composables"
    G
    H
    I
    J
    end
    
    subgraph "UI Components"
    M
    N
    Q
    end
    
    subgraph "Manual Flow (Search)"
    O
    end
Loading

@vercel
Copy link

vercel bot commented Oct 3, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Updated (UTC)
frontends-demo Ready Ready Preview Oct 30, 2025 8:48am
frontends-vue-starter-template Building Building Preview Oct 30, 2025 8:48am
1 Skipped Deployment
Project Deployment Preview Updated (UTC)
shopware-frontends-docs Skipped Skipped Oct 30, 2025 8:48am

@mkucmus mkucmus added the pkg-pr-publish invoke publishing of specific PR to test that version label Oct 3, 2025
@mkucmus mkucmus requested a review from Copilot October 3, 2025 15:13
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

Refactored the monolithic useListing composable into smaller, focused modules to improve modularity, maintainability, and SSR support for product listings. The main goal is to enable early data access to avoid ClientOnly loading without hydration mismatches.

  • Split single large composable into focused, single-responsibility modules
  • Added SSR optimization with early data extraction from CMS pages
  • Improved filter component UI consistency and animations

Reviewed Changes

Copilot reviewed 17 out of 17 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
packages/helpers/src/cms/getProductListingFromCmsPage.ts New helper to extract product listing data from CMS page structure for SSR optimization
packages/composables/src/useListing/useListingCore.ts Core state management with provide/inject pattern and shared state
packages/composables/src/useListing/useProductListingProducts.ts Products data and search operations module
packages/composables/src/useListing/useProductListingPagination.ts Pagination logic module
packages/composables/src/useListing/useProductListingSorting.ts Sorting functionality module
packages/composables/src/useListing/useProductListingFilters.ts Filter management module
packages/composables/src/useListing/utils.ts Shared types and utility functions
packages/composables/src/useListing/useListing.ts Refactored facade that composes all modular composables
packages/cms-base-layer/app/components/public/cms/CmsPage.vue Extract listing early and create context with initial listing
packages/cms-base-layer/app/components/SwProductListingFilters.vue Removed ClientOnly wrappers and improved filter state management

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

await core.search(
Object.assign(
{
page,
Copy link

Copilot AI Oct 3, 2025

Choose a reason for hiding this comment

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

The property should be 'p' instead of 'page' to match the API parameter format used elsewhere in the codebase (see line 116 in useProductListingProducts.ts where 'p: currentPage + 1' is used).

Suggested change
page,
p: page,

Copilot uses AI. Check for mistakes.
Comment on lines 58 to 59
<Transition name="fade">
<div v-if="isFilterVisible" class="self-stretch pt-6">
Copy link

Copilot AI Oct 3, 2025

Choose a reason for hiding this comment

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

The transition should include proper ARIA attributes for screen readers. Consider adding aria-expanded to the button and aria-hidden to the transition content to improve accessibility for users with disabilities.

Copilot uses AI. Check for mistakes.
Comment on lines 62 to 63
<Transition name="fade">
<div v-if="isFilterVisible" class="self-stretch flex flex-col justify-start items-start gap-4">
Copy link

Copilot AI Oct 3, 2025

Choose a reason for hiding this comment

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

The transition should include proper ARIA attributes for screen readers. Consider adding aria-expanded to the button and aria-hidden to the transition content to improve accessibility for users with disabilities.

Copilot uses AI. Check for mistakes.
Comment on lines 62 to 67
<Transition name="fade">
<div
v-if="isFilterVisible"
:id="props.filter.code"
class="self-stretch flex flex-col justify-start items-start gap-4"
>
Copy link

Copilot AI Oct 3, 2025

Choose a reason for hiding this comment

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

The transition should include proper ARIA attributes for screen readers. Consider adding aria-expanded to the button and aria-hidden to the transition content to improve accessibility for users with disabilities.

Copilot uses AI. Check for mistakes.
Comment on lines 145 to 146
<Transition name="fade">
<div v-if="isFilterVisible" :id="props.filter.code" class="self-stretch flex flex-col justify-start items-start gap-2.5">
Copy link

Copilot AI Oct 3, 2025

Choose a reason for hiding this comment

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

The transition should include proper ARIA attributes for screen readers. Consider adding aria-expanded to the button and aria-hidden to the transition content to improve accessibility for users with disabilities.

Copilot uses AI. Check for mistakes.
Comment on lines +45 to +52
for (const slot of block.slots) {
if (slot.type === "product-listing") {
const listing = slot.data?.listing;
if (listing) {
return listing as T;
}
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
for (const slot of block.slots) {
if (slot.type === "product-listing") {
const listing = slot.data?.listing;
if (listing) {
return listing as T;
}
}
}
const listing = block.slots
.find(slot => slot.type === "product-listing")
?.data?.listing as T | undefined;
if (listing) {
return listing;
}

Comment on lines +28 to +35
sections: Array<{
blocks?: Array<{
slots?: Array<{
type?: string;
data?: Record<string, unknown> | null;
}>;
}>;
}>;
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
sections: Array<{
blocks?: Array<{
slots?: Array<{
type?: string;
data?: Record<string, unknown> | null;
}>;
}>;
}>;
sections: {
blocks?: {
slots?: {
type?: string;
data?: Record<string, unknown> | null;
}[];
}[];
}[];

Maybe this way will be more readable?

order: string,
query?: operations["searchPage post /search"]["body"],
) => {
await core.search(
Copy link
Contributor

Choose a reason for hiding this comment

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

We had a discussion to not use API calls inside the composable. To make them more flexible and not dependent on the API client

initialListing: Schemas["ProductListingResult"],
): Promise<void>;
/**
* @deprecated - use `search` instead
Copy link
Contributor

Choose a reason for hiding this comment

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

New composable and deprecation at the beginning :D?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pkg-pr-publish invoke publishing of specific PR to test that version

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEATURE] Refactor listing composable

3 participants