Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore(headless): add a wider range of types for the ssr-commerce summaryState #4643

Merged
merged 18 commits into from
Nov 11, 2024
Merged
Show file tree
Hide file tree
Changes from 16 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
16 changes: 10 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 7 additions & 3 deletions packages/headless-react/README.md
Original file line number Diff line number Diff line change
@@ -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

<!-- TODO: KIT-3698: Add link to headless-react/ssr-commerce link in public doc -->

- 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.
13 changes: 9 additions & 4 deletions packages/headless-react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -39,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",
Expand All @@ -54,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"
Expand Down
65 changes: 65 additions & 0 deletions packages/headless-react/src/ssr-commerce/client-utils.ts
Original file line number Diff line number Diff line change
@@ -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> = () => T;

/**
* Determine if the given list of dependencies has changed.
*/
function useHasDepsChanged(deps: DependencyList) {
const ref = useRef<DependencyList | null>(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<T>(
subscribe: Subscriber,
getSnapshot: SnapshotGetter<T>
): T {
const snapshot = useRef<T | null>(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;
}
102 changes: 102 additions & 0 deletions packages/headless-react/src/ssr-commerce/commerce-engine.tsx
Original file line number Diff line number Diff line change
@@ -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<CommerceEngine, Controller>,
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<CommerceEngine, Controller>,
TSolutionType extends SolutionType = SolutionType,
>() {
return singleton(() =>
React.createContext<ContextState<
CommerceEngine,
TControllers,
TSolutionType
> | 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<CommerceEngine, Controller>,
>(options: CommerceEngineDefinitionOptions<TControllers>) {
const singletonContext = createSingletonContext<TControllers>();

type ContextStateType<TSolutionType extends SolutionType> = SingletonGetter<
React.Context<ContextState<
CommerceEngine,
TControllers,
TSolutionType
> | null>
>;
type ListingContext = ContextStateType<SolutionType.listing>;
type SearchContext = ContextStateType<SolutionType.search>;
type StandaloneContext = ContextStateType<SolutionType.standalone>;

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
),
},
};
}
Loading
Loading