-
Notifications
You must be signed in to change notification settings - Fork 34
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
feat(commerce-ssr): add parameter manager #4626
base: master
Are you sure you want to change the base?
Changes from 14 commits
d9151c9
1192473
34a8767
7277180
868593c
3a376d7
0a668dd
3f5d38e
8886515
bd64354
8992c30
a1d7ce4
9f8a1d2
e656536
61ffded
81d4e86
c0d38fe
6d1028c
64e3cba
f823d86
47ab808
8ea8256
49bf3d7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
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. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
'use client'; | ||
|
||
import { | ||
DependencyList, | ||
useCallback, | ||
useEffect, | ||
useMemo, | ||
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; | ||
} | ||
|
||
function getUrl() { | ||
if (typeof window === 'undefined') { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we need this despite the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. window is undefined in 'use client' components when they are first rendered on the server. I am not sure how this works in the context of a hook 🤔 . There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If I remove the check, the server responds with 500 (ReferenceError: document is not defined) upon the initial GET request. |
||
return null; | ||
} | ||
return new URL(document.location.href); | ||
} | ||
|
||
export function useAppHistoryRouter() { | ||
const [url, updateUrl] = useReducer(() => getUrl(), getUrl()); | ||
useEffect(() => { | ||
window.addEventListener('popstate', updateUrl); | ||
return () => window.removeEventListener('popstate', updateUrl); | ||
}, []); | ||
const replace = useCallback( | ||
(href: string) => window.history.replaceState(null, document.title, href), | ||
[] | ||
); | ||
const push = useCallback( | ||
(href: string) => window.history.pushState(null, document.title, href), | ||
[] | ||
); | ||
return useMemo(() => ({url, replace, push}), [url, replace, push]); | ||
} |
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 | ||
), | ||
}, | ||
}; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In https://docs.coveo.com/en/headless/latest/usage/headless-server-side-rendering/implement-search-parameter-support/#implement-a-history-router-hook, we explain how to implement this.
I thought we might as well create and export a hook for that to reduce boilerplating.