Skip to content

Commit

Permalink
add parameter manager
Browse files Browse the repository at this point in the history
  • Loading branch information
fpbrault committed Dec 17, 2024
1 parent 2c0b1a3 commit a2da694
Show file tree
Hide file tree
Showing 6 changed files with 128 additions and 8 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import {useParameterManager} from '@/lib/commerce-engine';
import {buildParameterSerializer} from '@coveo/headless-react/ssr-commerce';
import {useSearchParams, useLocation} from '@remix-run/react';
import {useEffect, useMemo, useRef} from 'react';

export default function ParameterManager({url}: {url: string | null}) {
const {state, methods} = useParameterManager();

const {serialize, deserialize} = buildParameterSerializer();

const location = useLocation();

const initialUrl = useMemo(() => new URL(url ?? location.pathname), [url]);
const previousUrl = useRef(initialUrl.href);
const [searchParams] = useSearchParams();

/**
* This flag serves to ensure that history navigation between pages does not clear commerce parameters and result in
* history state loss.
*
* When navigating to a new page, the ParameterManager controller is rebuilt with its initial state. 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 were not 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 are able to prevent this.
*
* For instance, suppose that a user initially navigates to /search?q=test. They then select the next page of results
* so that the URL becomes /search?q=test&page=1. Then, they navigate to a product page (e.g., /product/123). At this
* point, if they use their browser history to go back to the search page, the URL will be /search?q=test&page=1, but
* the ParameterManager controller's state will have been reset to only include the q=test parameter. Thanks to the
* flag, however, the navigation event will not cause the URL to be updated, but the useSearchParams hook will cause
* the controller to synchronize its state with the URL, thus preserving the page=1 parameter.
*/
const flag = useRef(true);

/**
* When the URL search parameters change, this effect deserializes them and synchronizes them into the
* ParameterManager controller's state.
*/
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);
}, [deserialize, methods, searchParams, serialize]);

/**
* When the ParameterManager controller's state changes, this effect serializes it into the URL and pushes the new URL
* to the browser history.
* */
useEffect(() => {
// Ensures that the effect only executes if the controller is hydrated, so that it plays well with the other effect.
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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export default function StandaloneSearchBox() {

useEffect(() => {
if (state.redirectTo === '/search') {
const url = `${state.redirectTo}#q=${encodeURIComponent(state.value)}`;
const url = `${state.redirectTo}?q=${encodeURIComponent(state.value)}`;
navigate(url, {preventScrollReset: true});
methods?.afterRedirection();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import ContextDropdown from '@/app/components/context-dropdown';
import FacetGenerator from '@/app/components/facets/facet-generator';
import Pagination from '@/app/components/pagination';
import ProductList from '@/app/components/product-list';
import {ListingProvider} from '@/app/components/providers/providers';
import Sort from '@/app/components/sort';
import StandaloneSearchBox from '@/app/components/standalone-search-box';
import Summary from '@/app/components/summary';
Expand All @@ -18,19 +19,25 @@ import {
toCoveoCartItems,
toCoveoCurrency,
} from '@/utils/external-api-conversions';
import {NavigatorContext} from '@coveo/headless-react/ssr-commerce';
import {
buildParameterSerializer,
NavigatorContext,
} from '@coveo/headless-react/ssr-commerce';
import {LoaderFunctionArgs} from '@remix-run/node';
import {useLoaderData, useParams} from '@remix-run/react';
import invariant from 'tiny-invariant';
import {ListingProvider} from '../components/providers/providers';
//import StandaloneSearchBox from '../components/standalone-search-box';
import {coveo_visitorId} from '../cookies.server';

export const loader = async ({params, request}: LoaderFunctionArgs) => {
invariant(params.listingId, 'Missing listingId parameter');

const navigatorContext = await getNavigatorContext(request);

const url = new URL(request.url);

const {deserialize} = buildParameterSerializer();
const parameters = deserialize(url.searchParams);

listingEngineDefinition.setNavigatorContextProvider(() => navigatorContext);

recommendationEngineDefinition.setNavigatorContextProvider(
Expand All @@ -47,6 +54,11 @@ export const loader = async ({params, request}: LoaderFunctionArgs) => {
items: toCoveoCartItems(await externalCartService.getItems()),
},
},
parameterManager: {
initialState: {
parameters: parameters,
},
},
context: {
language,
country,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import BreadcrumbManager from '@/app/components/breadcrumb-manager';
import ContextDropdown from '@/app/components/context-dropdown';
import DidYouMean from '@/app/components/did-you-mean';
import FacetGenerator from '@/app/components/facets/facet-generator';
import ParameterManager from '@/app/components/parameter-manager';
import ProductList from '@/app/components/product-list';
import {SearchProvider} from '@/app/components/providers/providers';
import SearchBox from '@/app/components/search-box';
Expand All @@ -17,13 +18,21 @@ import {
toCoveoCartItems,
toCoveoCurrency,
} from '@/utils/external-api-conversions';
import {NavigatorContext} from '@coveo/headless-react/ssr-commerce';
import {
buildParameterSerializer,
NavigatorContext,
} from '@coveo/headless-react/ssr-commerce';
import {LoaderFunctionArgs} from '@remix-run/node';
import {useLoaderData} from '@remix-run/react';

export const loader = async ({request}: LoaderFunctionArgs) => {
const navigatorContext = await getNavigatorContext(request);

const url = new URL(request.url);

const {deserialize} = buildParameterSerializer();
const parameters = deserialize(await url.searchParams);

searchEngineDefinition.setNavigatorContextProvider(() => navigatorContext);

const {country, currency, language} =
Expand All @@ -36,6 +45,11 @@ export const loader = async ({request}: LoaderFunctionArgs) => {
items: toCoveoCartItems(await externalCartService.getItems()),
},
},
parameterManager: {
initialState: {
parameters: parameters,
},
},
context: {
language,
country,
Expand All @@ -55,6 +69,7 @@ export default function SearchRoute() {
staticState: SearchStaticState;
navigatorContext: NavigatorContext;
}>();

return (
<SearchProvider
staticState={staticState}
Expand All @@ -74,6 +89,7 @@ export default function SearchRoute() {
<Summary />
<Sort />
<ProductList />
<ParameterManager url={navigatorContext.location} />
{/* 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
Expand Up @@ -20,7 +20,8 @@ import {
defineProductView,
getSampleCommerceEngineConfiguration,
defineDidYouMean,
defineRecommendations, //defineParameterManager,
defineRecommendations,
defineParameterManager,
} from '@coveo/headless-react/ssr-commerce';

type CommerceEngineConfig = CommerceEngineDefinitionOptions<
Expand Down Expand Up @@ -64,7 +65,7 @@ export default {
sort: defineSort(),
productView: defineProductView(),
didYouMean: defineDidYouMean(),
//parameterManager: defineParameterManager(),
parameterManager: defineParameterManager(),
facetGenerator: defineFacetGenerator(),
breadcrumbManager: defineBreadcrumbManager(),
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export const {
useSummary,
useFacetGenerator,
useBreadcrumbManager,
//useParameterManager,
useParameterManager,
} = engineDefinition.controllers;

export type ListingStaticState = InferStaticState<
Expand Down

0 comments on commit a2da694

Please sign in to comment.