Managing parameters
Managing parameters
This is for:
DeveloperWhen building your server-side rendered (SSR) commerce interfaces, you may want to manage URL parameters to synchronize them with the state of the interface. For example, when a user filters products on a search or product listing page (PLP), you might want to update the URL to reflect the applied filters.
The Headless React package provides a ParameterManager
controller that lets you manage URL parameters in your commerce application.
Create a hook on the ParameterManager
controller
Specify the ParameterManager
controller in your commerce engine configuration.
// lib/commerce-engine-config.ts
import {
defineParameterManager,
// ...
} from '@coveo/headless-react/ssr-commerce';
export default {
// ...
controllers: {
parameterManager: defineParameterManager(),
// ...
},
// ...
};
Retrieve the ParameterManager
controller hook after defining your engine.
// lib/commerce-engine.ts
import {defineCommerceEngine} from '@coveo/headless-react/ssr-commerce';
import engineConfig from './commerce-engine-config';
export const engineDefinition = defineCommerceEngine(engineConfig);
// ...
export const {
useParameterManager,
// ...
} = engineDefinition.controllers;
Create a component to manage URL parameters
Create hooks to:
-
Deserialize URL parameters and update the state.
-
Serialize URL parameters from the state and update the URL.
The Coveo Headless React package provides a buildParameterSerializer
function that you can use for that purpose, in addition to the ParameterManager
controller.
To avoid race conditions in Next.js, we recommend implementing your parameter manager component as follows:
// components/ParameterManager.tsx
'use client';
import {useParameterManager} from '@/lib/commerce-engine';
import {buildParameterSerializer} from '@coveo/headless-react/ssr-commerce';
import {useSearchParams} from 'next/navigation';
import {useEffect, useMemo, useRef} from 'react';
export default function ParameterManager({url}: {url: string | null}) {
const {state, methods} = useParameterManager();
const {serialize, deserialize} = buildParameterSerializer();
const initialUrl = useMemo(() => new URL(url ?? location.href), [url]);
const previousUrl = useRef(initialUrl.href);
const searchParams = useSearchParams();
const flag = useRef(true);
useEffect(() => {
if (methods === undefined) {
return;
}
if (flag.current) {
flag.current = false;
return;
}
const newCommerceParams = deserialize(searchParams);
const newUrl = serialize(newCommerceParams, new URL(previousUrl.current));
if (newUrl === previousUrl.current) {
return;
}
flag.current = true;
previousUrl.current = newUrl;
methods.synchronize(newCommerceParams);
}, [methods, serialize, deserialize, searchParams]);
useEffect(() => {
if (methods === undefined) {
return;
}
if (flag.current) {
flag.current = false;
return;
}
const newUrl = serialize(state.parameters, new URL(previousUrl.current));
if (previousUrl.current === newUrl) {
return;
}
flag.current = true;
previousUrl.current = newUrl;
history.pushState(null, document.title, newUrl);
}, [methods, serialize, state.parameters]);
return null;
}
This flag serves to ensure that history navigation between pages doesn’t clear commerce parameters and result in history state loss. | |
When the URL search parameters change, this effect deserializes them and synchronizes them into the ParameterManager controller’s state. |
|
Ensures that the effect only executes if the controller is hydrated, so that it plays well with the other effect. We do the same in the other effect. | |
Set the flag to true to prevent the other effect from executing in case of parallel triggers such as the URL changing and the controller state changing.
We do the same in the other effect. |
|
Watch the required dependencies from the parameter manager and parameter serializer.
Also watch the searchParams hook to trigger the deserialization effect when the URL search parameters change. |
|
When the ParameterManager controller’s state changes, this effect serializes it into the URL and pushes the new URL to the browser history. |
|
Watch the required dependencies from the parameter manager and parameter serializer.
Also watch the state.parameters to trigger the serialization effect when the controller state changes. |
Flag explanation
Next.js caches the static state of a page the first time it renders it. Consequently, if we serialize the state parameters and push them to the browser history when navigating back to a page, any commerce parameters in the URL that weren’t part of the controller’s initial state will be lost.
By having a guard that prevents effect execution when the flag is set to true
and sets the flag back to false
, we can prevent this.
Note
This flag is necessary in a Next.js app, but it might not be necessary in other frameworks. It’s not necessary and would even break with Remix, for example. It’s also possible that this flag become unnecessary or problematic in future versions of Next.js. |
The logic involved here is quite complex and the exact hydration sequence can vary between runs, so if you want to dive in, we recommend running the example project in the Headless project repository with breakpoints.
But, for a high-level explanation, consider the following example sequence.
-
A user initially navigates to
/search?q=test
. Next.js caches this page’s state with theq=test
parameter, for future visits to that page. Next.js then hydrates the page. -
Then, the user selects the next page of results so that the URL becomes
/search?q=test&page=1
. Dynamically, Next.js updates the state, but this new state isn’t cached for future visits to that page. -
Then, the user navigates to a product page (for example,
/product/123
). Next.js renders the page statically, caching the state, and then hydrates it. -
At this point, if the user uses their browser history to go back to the search page, the URL will be
/search?q=test&page=1
, but theParameterManager
controller’s state will have been reset to the cached state of that page (step 1), which only includes theq=test
parameter.However, thanks to the flag, the navigation event won’t cause the URL to be updated. Rather:
-
The
flag
is initially set totrue
. Just after the static rendering of the search page:-
The
useSearchParams
hook, which runs first because it’s defined first, tries to run, but is blocked by the flag. This also sets the flag tofalse
. -
The second hook then runs, because the flag is set to
false
, but is stopped at thepreviousUrl.current === newUrl
check, since in the cached static state, thepage=1
parameter isn’t part of the controller’s state.
-
-
Then, as the page hydrates, the flag is still set to
false
, so:-
The
useSearchParams
hook, which runs first because it’s defined first, causes the controller to synchronize its state with the URL, thus preserving thepage=1
parameter. This same hook also sets the flag totrue
. -
Since
state.parameters
has changed, the second hook triggers, attempting to update the URL, but doesn’t run because the flag has just been set totrue
.
-
-
Use your ParameterManager
component in your search and listing pages
Integrate your ParameterManager
component in your search and listing pages to manage the URL parameters, as in the following example.
// app/search/page.tsx
// ...
import ParameterManager from '@/components/parameter-manager';
import {buildParameterSerializer} from '@coveo/headless-react/ssr-commerce';
import {searchEngineDefinition} from '@/lib/commerce-engine';
import {NextJsNavigatorContext} from '@/lib/navigatorContextProvider';
export default async function Search({
searchParams,
}: {
searchParams: Promise<URLSearchParams>;
}) {
const navigatorContext = new NextJsNavigatorContext(headers());
searchEngineDefinition.setNavigatorContextProvider(() => navigatorContext);
// ...
const {deserialize} = buildParameterSerializer();
const parameters = deserialize(await searchParams);
const staticState = await searchEngineDefinition.fetchStaticState({
controllers: {
parameterManager: {initialState: {parameters}},
// ...
},
});
return (
<SearchProvider
staticState={staticState}
navigatorContext={navigatorContext.marshal}
>
<ParameterManager url={navigatorContext.location} />
<!--- ... --->
</SearchProvider>
);
}
Deserialize the URL parameters and set them as initial values for to the ParameterManager component when fetching the static state. |
|
Use the ParameterManager component to manage the URL parameters in your search page. |