Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 42 additions & 1 deletion samples/headless-ssr/commerce-nextjs/app/search/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import DidYouMean from '@/components/did-you-mean';
import FacetGenerator from '@/components/facets/facet-generator';
import ParameterManager from '@/components/parameter-manager';
import ProductList from '@/components/product-list';
import {AvailabilityProvider} from '@/components/providers/availability-provider';
import {SearchProvider} from '@/components/providers/providers';
import SearchBox from '@/components/search-box';
import ShowMore from '@/components/show-more';
Expand All @@ -15,6 +16,10 @@ import NotifyTrigger from '@/components/triggers/notify-trigger';
import QueryTrigger from '@/components/triggers/query-trigger';
import {searchEngineDefinition} from '@/lib/commerce-engine';
import {NextJsNavigatorContext} from '@/lib/navigatorContextProvider';
import {
type Availability,
fetchAvailability,
} from '@/lib/third-party-api-provider';
import {defaultContext} from '@/utils/context';

export default async function Search({
Expand Down Expand Up @@ -48,6 +53,38 @@ export default async function Search({
},
});

/**
* We elect to not block the rendering of the data of the system that handles our stock.
* Instead:
* - We do enqueue all requests
* - We "race them" against a 600ms dummy promise after_
* The latter represent the "budget" we allocate to the fake system before we continue on the rendering.
* When we do, future resolved values will be passed along using Suspense & Streaming.
*/

// The map is provided later on as a context as it will be used by our "suspensed" availability UI component
const productIdToAvailabilityRequests = new Map<
string,
Promise<Availability>
>();

// For each product, we enqueue the request and set it in the map we'll provide as context.
for (const product of staticState.controllers.productList.state.products) {
if (!product.ec_product_id) {
continue;
}
productIdToAvailabilityRequests.set(
product.ec_product_id,
fetchAvailability(product.ec_product_id)
);
}

// Then, we wait up to 600ms or for all requests to resolves. Any resolved request by the end of the 600ms will be usable during the initial rendering
// This is mostly done for demo purposes of the streaming process, and arbitrary delay such as this should _probably_ not be used in production implementation
await Promise.race([
Promise.allSettled(productIdToAvailabilityRequests.values()),
new Promise((resolve) => setTimeout(resolve, 600)),
]);
return (
<>
<h2>Search</h2>
Expand All @@ -69,7 +106,11 @@ export default async function Search({
<SearchBox />
<BreadcrumbManager />
<Summary />
<ProductList />
<AvailabilityProvider
productIdToAvailabilityRequests={productIdToAvailabilityRequests}
>
<ProductList />
</AvailabilityProvider>
{/* The ShowMore and Pagination components showcase two frequent ways to implement pagination. */}
{/* <Pagination
staticState={staticState.controllers.pagination.state}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
'use client';

import {Suspense, use} from 'react';
import {Availability} from '@/lib/third-party-api-provider';
import {useAvailability} from './providers/availability-provider';

function SuspendedCustomAvailabilityBadge({productId}: {productId: string}) {
const {getAvailabilityPromise} = useAvailability();
return (
<Suspense fallback={<div>Loading...</div>}>
<CustomAvailabilityBadge
availabilityPromise={getAvailabilityPromise(productId)}
/>
</Suspense>
);
}

const CustomAvailabilityBadge = ({
availabilityPromise,
}: {
availabilityPromise: Promise<Availability> | undefined;
}) => {
const availability = availabilityPromise ? use(availabilityPromise) : null;
switch (availability) {
case Availability.High:
return <div>High Availability</div>;
case Availability.Medium:
return <div>Medium Availability</div>;
case Availability.Low:
return <div>Low Availability</div>;
case Availability.OutOfStock:
return <div>Out of stock</div>;
default:
return <div>Unknown Availability</div>;
}
};

export {SuspendedCustomAvailabilityBadge as CustomAvailabilityBadge};
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import {useCart, useProductList} from '@/lib/commerce-engine';
import {addToCart} from '@/utils/cart';
import {CustomAvailabilityBadge} from './custom-availability-badge';
import ProductButtonWithImage from './product-button-with-image';

export default function ProductList() {
Expand All @@ -13,7 +14,9 @@ export default function ProductList() {
{state.products.map((product) => (
<li key={product.ec_product_id}>
<ProductButtonWithImage methods={methods} product={product} />

{product.ec_product_id && (
<CustomAvailabilityBadge productId={product.ec_product_id} />
)}
<button
type="button"
onClick={() => addToCart(cartMethods!, cartState, product, methods)}
Expand Down
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

AI alert for this; I used prop drillin' then asked Copilot to refactor this with a context.
If it smells, say so

Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
'use client';

import {createContext, type ReactNode, useContext} from 'react';
import type {Availability} from '@/lib/third-party-api-provider';

interface AvailabilityContextType {
getAvailabilityPromise: (
productId: string
) => Promise<Availability> | undefined;
}

const AvailabilityContext = createContext<AvailabilityContextType | undefined>(
undefined
);

interface AvailabilityProviderProps {
children: ReactNode;
productIdToAvailabilityRequests: Map<string, Promise<Availability>>;
}

export function AvailabilityProvider({
children,
productIdToAvailabilityRequests,
}: AvailabilityProviderProps) {
const getAvailabilityPromise = (
productId: string
): Promise<Availability> | undefined => {
return productIdToAvailabilityRequests.get(productId);
};

return (
<AvailabilityContext.Provider value={{getAvailabilityPromise}}>
{children}
</AvailabilityContext.Provider>
);
}

export function useAvailability() {
const context = useContext(AvailabilityContext);
if (context === undefined) {
throw new Error(
'useAvailability must be used within an AvailabilityProvider'
);
}
return context;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

I'd rename the file to "product-availability-api-provider"

Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/**
* @file
* A fake 3rd party API that returns the availability of a product based on its id.
*
* The returned value of this fake API are random.
* The "response time" of the API is however rigged to illustrate various scenarios:
Copy link
Contributor

Choose a reason for hiding this comment

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

Intuitively, I'd say that in a real-life scenario you'd probably be able to fetch this information in a single batch (i.e., get availability status for all products in the result list). Otherwise you'd really be hammering the external service with requests.

With that in mind, we could still simulate latency, but it could just be:

  • 1st call -> optimistic
  • 2nd call -> normal
  • 3rd call -> slow, and reset

Copy link
Collaborator Author

@louis-bompart louis-bompart Oct 16, 2025

Choose a reason for hiding this comment

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

Intuitively, I'd say that in a real-life scenario you'd probably be able to fetch this information in a single batch (i.e., get availability status for all products in the result list). Otherwise you'd really be hammering the external service with requests.

With that in mind, we could still simulate latency, but it could just be:

  • 1st call -> optimistic
  • 2nd call -> normal
  • 3rd call -> slow, and reset

Yeah we could do that. The goal with my current approach is that you can see suspense in action in one page load.

Copy link
Contributor

Choose a reason for hiding this comment

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

I understand, but I wonder if there's any use in demonstrating that unless we can think of a more realistic / common use case. It seems to me like an API that would accept only one product at a time would be incredibly inefficient.

* - The first and third request will respond 'fast' (<200ms) to represent the most optimistic cases
* - The fifth & sixth request will respond 'very slow' (>5s) to represent the worst case scenario case
* - The other request will respond between 500 & 1000ms.
* Every 10 requests, the counter of requests will reset.
*/

export enum Availability {
High = 'high',
Medium = 'medium',
Low = 'low',
OutOfStock = 'out-of-stock',
}
Comment on lines +13 to +18
Copy link
Contributor

Choose a reason for hiding this comment

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

Realistically, product availability would more likely be a discrete number of units, no?


const availabilities = Object.values(Availability);

let requestCounter = 0;

export const fetchAvailability = async (
productId: string
): Promise<Availability> => {
requestCounter++;
requestCounter %= 10;
const stableAvailability =
availabilities[Math.floor(Math.random() * availabilities.length)];
let latency = Math.random() * 500 + 500;
if (requestCounter === 1 || requestCounter === 3) {
latency = Math.random() * 200;
}
if (requestCounter === 5 || requestCounter === 6) {
latency += 4.5e3;
}
return await new Promise((resolve) => {
// Simulate fast network latency
setTimeout(() => {
console.log(
`🔗 Fetched availability for product ${productId}: ${stableAvailability}`
);
resolve(stableAvailability as Availability);
}, latency);
});
};
Loading