From d9151c995fee898974e6402feb3695685a73cbb9 Mon Sep 17 00:00:00 2001 From: ylakhdar Date: Thu, 31 Oct 2024 10:47:15 -0400 Subject: [PATCH 01/17] chore(headless SSR) add missing exports https://coveord.atlassian.net/browse/KIT-3694 --- packages/headless/src/app/commerce-ssr-engine/types/common.ts | 2 ++ .../headless/src/app/commerce-ssr-engine/types/core-engine.ts | 1 + packages/headless/src/ssr-commerce.index.ts | 4 ++++ 3 files changed, 7 insertions(+) diff --git a/packages/headless/src/app/commerce-ssr-engine/types/common.ts b/packages/headless/src/app/commerce-ssr-engine/types/common.ts index e19b4c9bde8..fdf21309c0e 100644 --- a/packages/headless/src/app/commerce-ssr-engine/types/common.ts +++ b/packages/headless/src/app/commerce-ssr-engine/types/common.ts @@ -6,11 +6,13 @@ import type { HasKey, InferControllerStaticStateMapFromControllers, InferControllerStaticStateFromController, + InferControllerPropsMapFromDefinitions, } from '../../ssr-engine/types/common.js'; export type { InferControllerStaticStateFromController, InferControllerStaticStateMapFromControllers, + InferControllerPropsMapFromDefinitions, }; export enum SolutionType { diff --git a/packages/headless/src/app/commerce-ssr-engine/types/core-engine.ts b/packages/headless/src/app/commerce-ssr-engine/types/core-engine.ts index 5c7dc8e06ec..a9d39706164 100644 --- a/packages/headless/src/app/commerce-ssr-engine/types/core-engine.ts +++ b/packages/headless/src/app/commerce-ssr-engine/types/core-engine.ts @@ -14,6 +14,7 @@ import { InferControllerStaticStateMapFromDefinitionsWithSolutionType, } from './common.js'; +export type {HydrateStaticState, FetchStaticState}; export type EngineDefinitionOptions< TOptions extends {configuration: EngineConfiguration}, TControllers extends ControllerDefinitionsMap< diff --git a/packages/headless/src/ssr-commerce.index.ts b/packages/headless/src/ssr-commerce.index.ts index dc14398af49..2ff89354831 100644 --- a/packages/headless/src/ssr-commerce.index.ts +++ b/packages/headless/src/ssr-commerce.index.ts @@ -88,6 +88,7 @@ export type { AnalyticsConfiguration, AnalyticsRuntimeEnvironment, } from './app/engine-configuration.js'; +export {SolutionType} from './app/commerce-ssr-engine/types/common.js'; export type { ControllerDefinitionsMap, InferControllerFromDefinition, @@ -95,6 +96,7 @@ export type { InferControllerStaticStateFromController, InferControllerStaticStateMapFromControllers, InferControllerStaticStateMapFromDefinitionsWithSolutionType, + InferControllerPropsMapFromDefinitions, } from './app/commerce-ssr-engine/types/common.js'; export type {Build} from './app/ssr-engine/types/build.js'; export type { @@ -102,6 +104,8 @@ export type { InferStaticState, InferHydratedState, InferBuildResult, + HydrateStaticState, + FetchStaticState, } from './app/commerce-ssr-engine/types/core-engine.js'; export type {LoggerOptions} from './app/logger.js'; export type {NavigatorContext} from './app/navigatorContextProvider.js'; From 1192473ebc39f82bf7c164ed39bd94d83c77ae19 Mon Sep 17 00:00:00 2001 From: ylakhdar Date: Thu, 31 Oct 2024 10:52:10 -0400 Subject: [PATCH 02/17] add ssr-commerce folder https://coveord.atlassian.net/browse/KIT-3695 --- packages/headless-react/package.json | 3 +- .../src/ssr-commerce/client-utils.ts | 65 +++++++ .../src/ssr-commerce/commerce-engine.tsx | 102 +++++++++++ .../src/ssr-commerce/common.tsx | 172 ++++++++++++++++++ .../headless-react/src/ssr-commerce/index.ts | 4 + .../headless-react/src/ssr-commerce/types.ts | 79 ++++++++ 6 files changed, 424 insertions(+), 1 deletion(-) create mode 100644 packages/headless-react/src/ssr-commerce/client-utils.ts create mode 100644 packages/headless-react/src/ssr-commerce/commerce-engine.tsx create mode 100644 packages/headless-react/src/ssr-commerce/common.tsx create mode 100644 packages/headless-react/src/ssr-commerce/index.ts create mode 100644 packages/headless-react/src/ssr-commerce/types.ts diff --git a/packages/headless-react/package.json b/packages/headless-react/package.json index 5c89bab71f1..f53b3f11e71 100644 --- a/packages/headless-react/package.json +++ b/packages/headless-react/package.json @@ -17,7 +17,8 @@ "license": "Apache-2.0", "type": "module", "exports": { - "./ssr": "./dist/ssr/index.js" + "./ssr": "./dist/ssr/index.js", + "./ssr-commerce": "./dist/ssr-commerce/index.js" }, "files": [ "dist" diff --git a/packages/headless-react/src/ssr-commerce/client-utils.ts b/packages/headless-react/src/ssr-commerce/client-utils.ts new file mode 100644 index 00000000000..c35d1f23732 --- /dev/null +++ b/packages/headless-react/src/ssr-commerce/client-utils.ts @@ -0,0 +1,65 @@ +'use client'; + +import {DependencyList, useEffect, useReducer, useRef} from 'react'; + +/** + * Subscriber is a function that takes a single argument, which is another function `listener` that returns `void`. The Subscriber function itself returns another function that can be used to unsubscribe the `listener`. + */ +export type Subscriber = (listener: () => void) => () => void; + +export type SnapshotGetter = () => T; + +/** + * Determine if the given list of dependencies has changed. + */ +function useHasDepsChanged(deps: DependencyList) { + const ref = useRef(null); + if (ref.current === null) { + ref.current = deps; + return false; + } + if ( + ref.current.length === deps.length && + !deps.some((dep, i) => !Object.is(ref.current![i], dep)) + ) { + return false; + } + ref.current = deps; + return true; +} + +/** + * Alternate for `useSyncExternalStore` which runs into infinite loops when hooks are used in `getSnapshot` + * https://github.com/facebook/react/issues/24529 + */ +export function useSyncMemoizedStore( + subscribe: Subscriber, + getSnapshot: SnapshotGetter +): T { + const snapshot = useRef(null); + const [, forceRender] = useReducer((s) => s + 1, 0); + + useEffect(() => { + let isMounted = true; + const unsubscribe = subscribe(() => { + if (isMounted) { + snapshot.current = getSnapshot(); + forceRender(); + } + }); + return () => { + isMounted = false; + unsubscribe(); + }; + }, [subscribe, getSnapshot]); + + // Since useRef does not take a dependencies array changes to dependencies need to be processed explicitly + if ( + useHasDepsChanged([subscribe, getSnapshot]) || + snapshot.current === null + ) { + snapshot.current = getSnapshot(); + } + + return snapshot.current; +} diff --git a/packages/headless-react/src/ssr-commerce/commerce-engine.tsx b/packages/headless-react/src/ssr-commerce/commerce-engine.tsx new file mode 100644 index 00000000000..76a0f755837 --- /dev/null +++ b/packages/headless-react/src/ssr-commerce/commerce-engine.tsx @@ -0,0 +1,102 @@ +import { + Controller, + CommerceEngine, + ControllerDefinitionsMap, + CommerceEngineDefinitionOptions, + defineCommerceEngine as defineBaseCommerceEngine, + CommerceEngineOptions, + SolutionType, +} from '@coveo/headless/ssr-commerce'; +// Workaround to prevent Next.js erroring about importing CSR only hooks +import React from 'react'; +import {singleton, SingletonGetter} from '../utils.js'; +import { + buildControllerHooks, + buildEngineHook, + buildHydratedStateProvider, + buildStaticStateProvider, +} from './common.js'; +import {ContextState, ReactEngineDefinition} from './types.js'; + +export type ReactCommerceEngineDefinition< + TControllers extends ControllerDefinitionsMap, + TSolutionType extends SolutionType, +> = ReactEngineDefinition< + CommerceEngine, + TControllers, + CommerceEngineOptions, + TSolutionType +>; + +// Wrapper to workaround the limitation that `createContext()` cannot be called directly during SSR in next.js +export function createSingletonContext< + TControllers extends ControllerDefinitionsMap, + TSolutionType extends SolutionType = SolutionType, +>() { + return singleton(() => + React.createContext | null>(null) + ); +} + +/** + * Returns controller hooks as well as SSR and CSR context providers that can be used to interact with a Commerce engine + * on the server and client side respectively. + */ +export function defineCommerceEngine< + TControllers extends ControllerDefinitionsMap, +>(options: CommerceEngineDefinitionOptions) { + const singletonContext = createSingletonContext(); + + type ContextStateType = SingletonGetter< + React.Context | null> + >; + type ListingContext = ContextStateType; + type SearchContext = ContextStateType; + type StandaloneContext = ContextStateType; + + const { + listingEngineDefinition, + searchEngineDefinition, + standaloneEngineDefinition, + } = defineBaseCommerceEngine({...options}); + return { + useEngine: buildEngineHook(singletonContext), + controllers: buildControllerHooks(singletonContext, options.controllers), + listingEngineDefinition: { + ...listingEngineDefinition, + StaticStateProvider: buildStaticStateProvider( + singletonContext as ListingContext + ), + + HydratedStateProvider: buildHydratedStateProvider( + singletonContext as ListingContext + ), + }, + searchEngineDefinition: { + ...searchEngineDefinition, + StaticStateProvider: buildStaticStateProvider( + singletonContext as SearchContext + ), + HydratedStateProvider: buildHydratedStateProvider( + singletonContext as SearchContext + ), + }, + standaloneEngineDefinition: { + ...standaloneEngineDefinition, + StaticStateProvider: buildStaticStateProvider( + singletonContext as StandaloneContext + ), + HydratedStateProvider: buildHydratedStateProvider( + singletonContext as StandaloneContext + ), + }, + }; +} diff --git a/packages/headless-react/src/ssr-commerce/common.tsx b/packages/headless-react/src/ssr-commerce/common.tsx new file mode 100644 index 00000000000..ca659653aba --- /dev/null +++ b/packages/headless-react/src/ssr-commerce/common.tsx @@ -0,0 +1,172 @@ +import { + Controller, + ControllerDefinitionsMap, + CoreEngineNext, + InferControllerFromDefinition, + InferControllerStaticStateMapFromDefinitionsWithSolutionType, + InferControllersMapFromDefinition, + SolutionType, +} from '@coveo/headless/ssr-commerce'; +import { + useContext, + useCallback, + useMemo, + Context, + PropsWithChildren, +} from 'react'; +import {SingletonGetter, capitalize, mapObject} from '../utils.js'; +import {useSyncMemoizedStore} from './client-utils.js'; +import { + ContextHydratedState, + ContextState, + ControllerHook, + InferControllerHooksMapFromDefinition, +} from './types.js'; + +export class MissingEngineProviderError extends Error { + static message = + 'Unable to find Context. Please make sure you are wrapping your component with either `StaticStateProvider` or `HydratedStateProvider` component that can provide the required context.'; + constructor() { + super(MissingEngineProviderError.message); + } +} + +function isHydratedStateContext< + TEngine extends CoreEngineNext, + TControllers extends ControllerDefinitionsMap, + TSolutionType extends SolutionType, +>( + ctx: ContextState +): ctx is ContextHydratedState { + return 'engine' in ctx; +} + +function buildControllerHook< + TEngine extends CoreEngineNext, + TControllers extends ControllerDefinitionsMap, + TKey extends keyof TControllers, + TSolutionType extends SolutionType, +>( + singletonContext: SingletonGetter< + Context | null> + >, + key: TKey +): ControllerHook> { + return () => { + const ctx = useContext(singletonContext.get()); + if (ctx === null) { + throw new MissingEngineProviderError(); + } + + // Workaround to ensure that 'key' can be used as an index for 'ctx.controllers'. A more robust solution is needed. + type ControllerKey = Exclude; + const subscribe = useCallback( + (listener: () => void) => + isHydratedStateContext(ctx) + ? ctx.controllers[key as ControllerKey].subscribe(listener) + : () => {}, + [ctx] + ); + const getStaticState = useCallback(() => ctx.controllers[key].state, [ctx]); + const state = useSyncMemoizedStore(subscribe, getStaticState); + const controller = useMemo(() => { + if (!isHydratedStateContext(ctx)) { + return undefined; + } + const controller = ctx.controllers[key as ControllerKey]; + const {state: _, subscribe: __, ...remainder} = controller; + return mapObject(remainder, (member) => + typeof member === 'function' ? member.bind(controller) : member + ) as Omit< + InferControllerFromDefinition, + 'state' | 'subscribe' + >; + }, [ctx, key]); + return {state, controller}; + }; +} + +export function buildControllerHooks< + TEngine extends CoreEngineNext, + TControllers extends ControllerDefinitionsMap, + TSolutionType extends SolutionType, +>( + singletonContext: SingletonGetter< + Context | null> + >, + controllersMap?: TControllers +) { + return ( + controllersMap + ? Object.fromEntries( + Object.keys(controllersMap).map((key) => [ + `use${capitalize(key)}`, + buildControllerHook(singletonContext, key), + ]) + ) + : {} + ) as InferControllerHooksMapFromDefinition; +} + +export function buildEngineHook< + TEngine extends CoreEngineNext, + TControllers extends ControllerDefinitionsMap, + TSolutionType extends SolutionType, +>( + singletonContext: SingletonGetter< + Context | null> + > +) { + return () => { + const ctx = useContext(singletonContext.get()); + if (ctx === null) { + throw new MissingEngineProviderError(); + } + return isHydratedStateContext(ctx) ? ctx.engine : undefined; + }; +} + +export function buildStaticStateProvider< + TEngine extends CoreEngineNext, + TControllers extends ControllerDefinitionsMap, + TSolutionType extends SolutionType, +>( + singletonContext: SingletonGetter< + Context | null> + > +) { + return ({ + controllers, + children, + }: PropsWithChildren<{ + controllers: InferControllerStaticStateMapFromDefinitionsWithSolutionType< + TControllers, + TSolutionType + >; + }>) => { + const {Provider} = singletonContext.get(); + return {children}; + }; +} + +export function buildHydratedStateProvider< + TEngine extends CoreEngineNext, + TControllers extends ControllerDefinitionsMap, + TSolutionType extends SolutionType, +>( + singletonContext: SingletonGetter< + Context | null> + > +) { + return ({ + engine, + controllers, + children, + }: PropsWithChildren<{ + engine: TEngine; + controllers: InferControllersMapFromDefinition; + }>) => { + const {Provider} = singletonContext.get(); + return {children}; + }; +} diff --git a/packages/headless-react/src/ssr-commerce/index.ts b/packages/headless-react/src/ssr-commerce/index.ts new file mode 100644 index 00000000000..2e2a5a56881 --- /dev/null +++ b/packages/headless-react/src/ssr-commerce/index.ts @@ -0,0 +1,4 @@ +export {defineCommerceEngine} from './commerce-engine.js'; +export type {ReactCommerceEngineDefinition} from './commerce-engine.js'; +export {MissingEngineProviderError} from './common.js'; +export * from '@coveo/headless/ssr-commerce'; diff --git a/packages/headless-react/src/ssr-commerce/types.ts b/packages/headless-react/src/ssr-commerce/types.ts new file mode 100644 index 00000000000..9511433e339 --- /dev/null +++ b/packages/headless-react/src/ssr-commerce/types.ts @@ -0,0 +1,79 @@ +import { + Controller, + ControllerDefinitionsMap, + InferControllerFromDefinition, + InferControllersMapFromDefinition, + InferControllerStaticStateMapFromDefinitionsWithSolutionType, + EngineDefinition, + SolutionType, + CoreEngineNext, +} from '@coveo/headless/ssr-commerce'; +import {FunctionComponent, PropsWithChildren} from 'react'; + +export type ContextStaticState< + TEngine extends CoreEngineNext, + TControllers extends ControllerDefinitionsMap, + TSolutionType extends SolutionType, +> = { + controllers: InferControllerStaticStateMapFromDefinitionsWithSolutionType< + TControllers, + TSolutionType + >; +}; + +export type ContextHydratedState< + TEngine extends CoreEngineNext, + TControllers extends ControllerDefinitionsMap, + TSolutionType extends SolutionType, +> = { + engine: TEngine; + controllers: InferControllersMapFromDefinition; +}; + +export type ContextState< + TEngine extends CoreEngineNext, + TControllers extends ControllerDefinitionsMap, + TSolutionType extends SolutionType, +> = + | ContextStaticState + | ContextHydratedState; + +export type ControllerHook = () => { + state: TController['state']; + controller?: Omit; +}; + +export type InferControllerHooksMapFromDefinition< + TControllers extends ControllerDefinitionsMap, +> = { + [K in keyof TControllers as `use${Capitalize< + K extends string ? K : never + >}`]: ControllerHook>; +}; + +export type ReactEngineDefinition< + TEngine extends CoreEngineNext, + TControllers extends ControllerDefinitionsMap, + TEngineOptions, + TSolutionType extends SolutionType, +> = EngineDefinition & { + controllers: InferControllerHooksMapFromDefinition; + useEngine(): TEngine | undefined; + StaticStateProvider: FunctionComponent< + PropsWithChildren<{ + controllers: InferControllerStaticStateMapFromDefinitionsWithSolutionType< + TControllers, + TSolutionType + >; + }> + >; + HydratedStateProvider: FunctionComponent< + PropsWithChildren<{ + engine: TEngine; + controllers: InferControllersMapFromDefinition< + TControllers, + TSolutionType + >; + }> + >; +}; From 34a87679b663a5dddd17e69ceaaa9123b371bc85 Mon Sep 17 00:00:00 2001 From: ylakhdar Date: Thu, 31 Oct 2024 11:05:30 -0400 Subject: [PATCH 03/17] update readme https://coveord.atlassian.net/browse/KIT-3695 --- packages/headless-react/README.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/headless-react/README.md b/packages/headless-react/README.md index b75d204cf12..16ccd9b3cf2 100644 --- a/packages/headless-react/README.md +++ b/packages/headless-react/README.md @@ -1,9 +1,13 @@ # Headless React Utils for SSR -`@coveo/headless-react/ssr` provides React utilities for server-side rendering with headless controllers. +`@coveo/headless-react` provides React utilities for server-side rendering with headless controllers. This package includes two sub-packages: + +- `@coveo/headless-react/ssr`: For general server-side rendering with headless controllers. +- `@coveo/headless-react/ssr-commerce`: For implementing a commerce storefront with server-side rendering. ## Learn more + + - Checkout our [Documentation](https://docs.coveo.com/en/headless/latest/usage/headless-server-side-rendering/) -- Refer to [samples/headless-ssr](https://github.com/coveo/ui-kit/tree/master/packages/samples/headless-ssr/) for examples. -- All exports from `@coveo/headless/ssr` are also available from under `@coveo/headless-react/ssr` as convenience. +- Refer to [samples/headless-ssr-commerce](https://github.com/coveo/ui-kit/tree/master/packages/samples/headless-ssr-commerce/) for examples. From 727718058c72ffea5e0fd5b2b54d6c46bbe562eb Mon Sep 17 00:00:00 2001 From: ylakhdar Date: Thu, 31 Oct 2024 11:41:50 -0400 Subject: [PATCH 04/17] use react hooks in sample https://coveord.atlassian.net/browse/KIT-3699 --- package-lock.json | 16 ++- packages/headless-react/package.json | 10 +- .../samples/headless-ssr-commerce/README.md | 3 +- .../app/_components/breadcrumb-manager.tsx | 23 +--- .../app/_components/cart.tsx | 41 ++----- .../app/_components/facets/category-facet.tsx | 4 +- .../app/_components/facets/date-facet.tsx | 4 +- .../_components/facets/facet-generator.tsx | 25 +--- .../app/_components/facets/numeric-facet.tsx | 4 +- .../app/_components/facets/regular-facet.tsx | 4 +- .../app/_components/instant-product.tsx | 28 +---- .../app/_components/pages/listing-page.tsx | 116 +++++------------- .../app/_components/pages/product-page.tsx | 7 +- .../app/_components/pages/recommendation.tsx | 55 --------- .../app/_components/pages/search-page.tsx | 116 +++++------------- .../app/_components/pagination.tsx | 21 +--- .../app/_components/product-list.tsx | 27 +--- .../app/_components/recent-queries.tsx | 27 +--- .../app/_components/recommendation-list.tsx | 30 ++--- .../app/_components/search-box.tsx | 68 +++------- .../app/_components/show-more.tsx | 33 +---- .../app/_components/sort.tsx | 43 +++---- .../app/_components/standalone-search-box.tsx | 67 +++------- .../app/_components/summary.tsx | 37 ++---- .../_components/triggers/notify-trigger.tsx | 24 +--- .../_components/triggers/query-trigger.tsx | 23 +--- .../triggers/redirection-trigger.tsx | 24 +--- .../app/_components/triggers/triggers.tsx | 40 +----- .../app/_lib/commerce-engine-config.ts | 4 +- .../app/_lib/commerce-engine.ts | 27 +++- .../app/_lib/navigatorContextProvider.ts | 2 +- .../headless-ssr-commerce/app/layout.tsx | 3 +- .../app/listing/page.tsx | 38 +++++- .../app/recommendation/page.tsx | 29 ----- .../headless-ssr-commerce/app/search/page.tsx | 33 ++++- .../headless-ssr-commerce/package.json | 4 +- 36 files changed, 317 insertions(+), 743 deletions(-) delete mode 100644 packages/samples/headless-ssr-commerce/app/_components/pages/recommendation.tsx delete mode 100644 packages/samples/headless-ssr-commerce/app/recommendation/page.tsx diff --git a/package-lock.json b/package-lock.json index ae7dc3bdd6e..133d075bb84 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20900,7 +20900,7 @@ "version": "0.16.8", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz", "integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==", - "dev": true + "devOptional": true }, "node_modules/@types/semver": { "version": "7.5.8", @@ -57813,8 +57813,6 @@ "@coveo/release": "1.0.0", "@testing-library/react": "14.3.1", "@types/jest": "29.5.12", - "@types/react": "18.3.3", - "@types/react-dom": "18.3.0", "@typescript-eslint/eslint-plugin": "7.17.0", "eslint-plugin-jest-dom": "5.4.0", "eslint-plugin-react": "7.35.0", @@ -57829,7 +57827,13 @@ "engines": { "node": "^20.9.0" }, + "optionalDependencies": { + "@types/react": "^18.0.0", + "@types/react-dom": "^18.0.0" + }, "peerDependencies": { + "@types/react": "18.3.3", + "@types/react-dom": "18.3.0", "react": "^18", "react-dom": "^18" } @@ -57856,7 +57860,7 @@ "version": "18.2.21", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.21.tgz", "integrity": "sha512-neFKG/sBAwGxHgXiIxnbm3/AAVQ/cMRS93hvBpg8xYRbeQSPVABp9U2bRnPf0iI4+Ucdv3plSxKK+3CW2ENJxA==", - "dev": true, + "devOptional": true, "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -57867,7 +57871,7 @@ "version": "18.2.7", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.7.tgz", "integrity": "sha512-GRaAEriuT4zp9N4p1i8BDBYmEyfo+xQ3yHjJU4eiK5NDa1RmUZG+unZABUTK4/Ox/M+GaHwb6Ow8rUITrtjszA==", - "dev": true, + "devOptional": true, "dependencies": { "@types/react": "*" } @@ -65142,7 +65146,7 @@ "name": "@coveo/headless-ssr-commerce-samples", "version": "0.0.0", "dependencies": { - "@coveo/headless": "3.5.0", + "@coveo/headless-react": "2.0.8", "next": "14.2.5", "react": "^18.2.0", "react-dom": "^18.2.0" diff --git a/packages/headless-react/package.json b/packages/headless-react/package.json index f53b3f11e71..e7bbec2da38 100644 --- a/packages/headless-react/package.json +++ b/packages/headless-react/package.json @@ -40,8 +40,6 @@ "@coveo/release": "1.0.0", "@testing-library/react": "14.3.1", "@types/jest": "29.5.12", - "@types/react": "18.3.3", - "@types/react-dom": "18.3.0", "@typescript-eslint/eslint-plugin": "7.17.0", "eslint-plugin-jest-dom": "5.4.0", "eslint-plugin-react": "7.35.0", @@ -55,7 +53,13 @@ }, "peerDependencies": { "react": "^18", - "react-dom": "^18" + "react-dom": "^18", + "@types/react": "18.3.3", + "@types/react-dom": "18.3.0" + }, + "optionalDependencies": { + "@types/react": "^18.0.0", + "@types/react-dom": "^18.0.0" }, "engines": { "node": "^20.9.0" diff --git a/packages/samples/headless-ssr-commerce/README.md b/packages/samples/headless-ssr-commerce/README.md index a749fa88c3f..e60a95e80e8 100644 --- a/packages/samples/headless-ssr-commerce/README.md +++ b/packages/samples/headless-ssr-commerce/README.md @@ -1,7 +1,6 @@ # Server side rendering examples -- Demonstrates usage of the framework agnostic `@coveo/headless/ssr-commerce` utils for Server-Side Rendering with headless using Next.js in a commerce app. -- Although Next.js is used to demonstrate SSR usage for convenience, the utils are not specific to Next.js. +- Demonstrates usage of the framework `@coveo/headless-react/ssr-commerce` utils for Server-Side Rendering with headless using Next.js in a commerce app. ## Getting Started diff --git a/packages/samples/headless-ssr-commerce/app/_components/breadcrumb-manager.tsx b/packages/samples/headless-ssr-commerce/app/_components/breadcrumb-manager.tsx index 0ea61e0d277..01a5f8713c9 100644 --- a/packages/samples/headless-ssr-commerce/app/_components/breadcrumb-manager.tsx +++ b/packages/samples/headless-ssr-commerce/app/_components/breadcrumb-manager.tsx @@ -1,26 +1,15 @@ +'use client'; + import { - BreadcrumbManagerState, NumericFacetValue, DateFacetValue, CategoryFacetValue, - BreadcrumbManager as HeadlessBreadcrumbManager, RegularFacetValue, -} from '@coveo/headless/ssr-commerce'; -import {useEffect, useState} from 'react'; - -interface BreadcrumbManagerProps { - staticState: BreadcrumbManagerState; - controller?: HeadlessBreadcrumbManager; -} - -export default function BreadcrumbManager(props: BreadcrumbManagerProps) { - const {staticState, controller} = props; - - const [state, setState] = useState(staticState); +} from '@coveo/headless-react/ssr-commerce'; +import {useBreadcrumbManager} from '../_lib/commerce-engine'; - useEffect(() => { - controller?.subscribe(() => setState(controller.state)); - }, [controller]); +export default function BreadcrumbManager() { + const {state, controller} = useBreadcrumbManager(); const renderBreadcrumbValue = ( value: diff --git a/packages/samples/headless-ssr-commerce/app/_components/cart.tsx b/packages/samples/headless-ssr-commerce/app/_components/cart.tsx index d270c7ad04e..55274cb7b38 100644 --- a/packages/samples/headless-ssr-commerce/app/_components/cart.tsx +++ b/packages/samples/headless-ssr-commerce/app/_components/cart.tsx @@ -1,39 +1,12 @@ -import { - Cart as CartController, - CartItem, - CartState, - ContextState, - Context as ContextController, -} from '@coveo/headless/ssr-commerce'; -import {useEffect, useState} from 'react'; -import {formatCurrency} from '../_utils/format-currency'; +'use client'; -interface CartProps { - staticState: CartState; - controller?: CartController; - staticContextState: ContextState; - contextController?: ContextController; -} +import {CartItem} from '@coveo/headless-react/ssr-commerce'; +import {useCart, useContext} from '../_lib/commerce-engine'; +import {formatCurrency} from '../_utils/format-currency'; -export default function Cart({ - staticState, - controller, - staticContextState, - contextController, -}: CartProps) { - const [state, setState] = useState(staticState); - useEffect( - () => controller?.subscribe(() => setState({...controller.state})), - [controller] - ); - const [contextState, setContextState] = useState(staticContextState); - useEffect( - () => - contextController?.subscribe(() => - setContextState({...contextController.state}) - ), - [contextController] - ); +export default function Cart() { + const {state, controller} = useCart(); + const {state: contextState} = useContext(); const adjustQuantity = (item: CartItem, delta: number) => { controller?.updateItemQuantity({ diff --git a/packages/samples/headless-ssr-commerce/app/_components/facets/category-facet.tsx b/packages/samples/headless-ssr-commerce/app/_components/facets/category-facet.tsx index 90c499d7629..418cbf91593 100644 --- a/packages/samples/headless-ssr-commerce/app/_components/facets/category-facet.tsx +++ b/packages/samples/headless-ssr-commerce/app/_components/facets/category-facet.tsx @@ -1,9 +1,11 @@ +'use client'; + import { CategoryFacetSearchResult, CategoryFacetState, CategoryFacetValue, CategoryFacet as HeadlessCategoryFacet, -} from '@coveo/headless/ssr-commerce'; +} from '@coveo/headless-react/ssr-commerce'; import {useEffect, useRef, useState} from 'react'; interface ICategoryFacetProps { diff --git a/packages/samples/headless-ssr-commerce/app/_components/facets/date-facet.tsx b/packages/samples/headless-ssr-commerce/app/_components/facets/date-facet.tsx index e6e944c7148..1541bbda894 100644 --- a/packages/samples/headless-ssr-commerce/app/_components/facets/date-facet.tsx +++ b/packages/samples/headless-ssr-commerce/app/_components/facets/date-facet.tsx @@ -1,7 +1,9 @@ +'use client'; + import { DateFacetState, DateFacet as HeadlessDateFacet, -} from '@coveo/headless/ssr-commerce'; +} from '@coveo/headless-react/ssr-commerce'; import {useEffect, useState} from 'react'; interface IDateFacetProps { diff --git a/packages/samples/headless-ssr-commerce/app/_components/facets/facet-generator.tsx b/packages/samples/headless-ssr-commerce/app/_components/facets/facet-generator.tsx index 4c0ce296bf0..0d6dcbb26bf 100644 --- a/packages/samples/headless-ssr-commerce/app/_components/facets/facet-generator.tsx +++ b/packages/samples/headless-ssr-commerce/app/_components/facets/facet-generator.tsx @@ -1,28 +1,13 @@ -import { - FacetGenerator as HeadlessFacetGenerator, - FacetGeneratorState, -} from '@coveo/headless/ssr-commerce'; -import {useEffect, useState} from 'react'; +'use client'; + +import {useFacetGenerator} from '@/app/_lib/commerce-engine'; import CategoryFacet from './category-facet'; import DateFacet from './date-facet'; import NumericFacet from './numeric-facet'; import RegularFacet from './regular-facet'; -interface IFacetGeneratorProps { - controller?: HeadlessFacetGenerator; - staticState: FacetGeneratorState; -} - -export default function FacetGenerator(props: IFacetGeneratorProps) { - const {controller, staticState} = props; - - const [state, setState] = useState(staticState); - - useEffect(() => { - controller?.subscribe(() => { - setState(controller.state); - }); - }, [controller]); +export default function FacetGenerator() { + const {state, controller} = useFacetGenerator(); return (