Add a feature package
A feature package is one storefront feature, ready to use: shopping lists,
quotes, returns, invoices. This page adds
feature-shopping-list to a
project that does not have it yet. Every other feature-* package follows the
same steps.
What a feature package brings
Section titled “What a feature package brings”Each feature has the same shape. You import each part from its own subpath:
| Subpath | What it is |
|---|---|
. |
Types, cache keys and helpers. Safe to import in a Server Component. |
./queries |
queryOptions factories and the GraphQL documents. Also server-safe. |
./hooks |
Client hooks for reads and mutations. |
./provider, ./context |
The provider that gives the feature its session, and the hooks that read it. |
./<component> |
One subpath per component, for example ./shopping-list-detail. The smaller parts of a component come from the same subpath. |
./styles.css |
Registers the package’s source with Tailwind. |
./messages/* |
Translations in en, nl, de, fr and es, under one namespace (ShoppingList). |
Not every feature has every subpath: feature-auth has no ./queries, and
feature-content-blocks has no provider. The package page in the reference
lists what yours has.
The packages ship TypeScript source, not a build. Your app compiles them, which is why some of the steps below exist.
Before you start
Section titled “Before you start”- Your project is a copy of a reference site (
evolve-reference-b2corevolve-reference-b2b), with the Evolve registry configured. See Installation. - Read the package’s README. Its GraphQL contract section says which operations the feature calls.
1. Serve the feature’s GraphQL
Section titled “1. Serve the feature’s GraphQL”A feature only calls GraphQL. Some backend module has to answer those calls.
Open the feature’s domain in the GraphQL reference and
read Served by. For shopping lists that is
Shopping list GraphQL: the
ShoppingListModule of
commercetools,
Medusa and
SFCC.
Each operation page shows what every engine does with it. On SFCC, the shopping list reads resolve, but the writes are stubs that answer Not implemented.
Add the module to the subgraph that owns the customer’s data. In the reference
sites that is backend/purchase, and the module goes in its CompositeModule:
import { ShoppingListModule } from "@evolve-framework/commercetools";
export const createModule = (payments?: PaymentRegistry): CompositeModule<ContextValue> => { return new CompositeModule<ContextValue>([ // ...the modules you already have new ShoppingListModule(), ]);};On Medusa, the module also needs the
@evolve-framework/medusa-plugin-wishlist
plugin in your Medusa server. Without it, every shopping list call fails.
2. Install and wire the package
Section titled “2. Install and wire the package”-
Install it in
frontend/site:pnpm add @evolve-storefront/feature-shopping-listThe peers (
react,@tanstack/react-query,@labdigital/graphql-fetcher,graphql,next-intl,sonnerandnext) are already in a reference-site copy. The reference sites keep every version in the pnpm catalog inpnpm-workspace.yaml, so the entry inpackage.jsonreads"catalog:". -
Let Next compile it. Add the package to
transpilePackages:transpilePackages: ["@evolve-storefront/ui","@evolve-storefront/core","@evolve-storefront/errors","@evolve-storefront/feature-shopping-list",// ...],The package depends on
ui,coreanderrors. They are in the list already if you have any other feature. -
Import its styles in your brand stylesheet, after
ui:@import "tailwindcss";@import "@evolve-storefront/ui/styles.css";@import "@evolve-storefront/feature-shopping-list/styles.css";This line adds
@source "./src"for the package, so Tailwind generates the classes its components use. If you forget it, you get no error. The components only look a little broken. -
Render the provider in
src/app/[locale]/providers.tsx. It must sit insideNextIntlClientProvider,ClientGqlFetcherProviderandQueryClientProvider. The reference sites wrap it in a small component that reads the app’s own auth hooks:import { ShoppingListProvider } from "@evolve-storefront/feature-shopping-list/provider";import { useEnsureSession } from "#hooks/auth/use-ensure-session.ts";import { useAuth } from "#hooks/use-auth.ts";function ShoppingListSessionProvider({ children }: { children: ReactNode }) {const { isAuthenticated, hasToken, validateLocalToken, values } = useAuth();const ensureSession = useEnsureSession();return (<ShoppingListProviderisAuthenticated={isAuthenticated}hasToken={hasToken}businessUnitId={values?.values.businessContext?.businessUnit}ensureSession={ensureSession}onTokenChange={validateLocalToken}>{children}</ShoppingListProvider>);}Then nest
<ShoppingListSessionProvider>with the other feature providers, aroundchildren.businessUnitIdsets the mode. Without it, a shopper has one list behind a heart toggle. With it, an associate has the company’s named lists behind a picker. Without the provider, the feature acts as if every visitor is an anonymous guest. -
Generate the messages and types. From the repository root:
pnpm codegenThis builds the backend schema and the supergraph, then runs the site’s
codegen:typesandcodegen:messages.codegen:messagesrunsevolve messages build. It reads every@evolve-storefront/*package in the site’spackage.jsonand adds theShoppingListkeys to each file insrc/messages/. A key you already have keeps your value.codegen:typeschecks the feature’s GraphQL documents against your supergraph, and adds them to the persisted-document manifest.
When codegen fails on a feature document
Section titled “When codegen fails on a feature document”If codegen fails on a field like anonymousShoppingList, your supergraph does
not serve an operation the feature ships. You have two choices:
- Add the module that serves it (step 1).
- If no session in your shop can reach that operation, drop it in
frontend/site/codegen/feature-documents.cjs. Add the operation name tounreachableOperations, and any fragment that only it uses tounreachableFragments.
The reference sites already drop GetAnonymousShoppingList and
AnonymousShoppingListCreate, because no engine serves anonymousShoppingList.
That is the shared, read-only list. The share button and
shoppingListDetailQueryOptions({ shareable: true }) depend on it, so do not
use them.
3. Put it on a page
Section titled “3. Put it on a page”The feature’s views never fetch. Your page runs the query and gives the view the data. You also write the markup for each line item, so the line items look like the rest of your site.
A button on product surfaces
Section titled “A button on product surfaces”The package ships two buttons and no wrapper that picks between them, because the mode is your app’s decision. The reference sites decide in one component and use it on the product card, the product page and the carousel card:
"use client";
import { useShoppingListContext } from "@evolve-storefront/feature-shopping-list/context";import { ShoppingListAddButton } from "@evolve-storefront/feature-shopping-list/shopping-list-add-button";import { ShoppingListPickerButton } from "@evolve-storefront/feature-shopping-list/shopping-list-picker-button";import type { ReactNode } from "react";
type Props = { sku: string; productName?: string; className?: string };
export function ShoppingListButton({ sku, productName, className }: Props): ReactNode { const { businessUnitId } = useShoppingListContext();
if (businessUnitId) { return <ShoppingListPickerButton sku={sku} productName={productName} className={className} />; }
return <ShoppingListAddButton sku={sku} productName={productName} className={className} />;}productName appears in the toast after an add. Toasts need a <Toaster />
in your layout. The reference sites render the one from sonner in
src/app/[locale]/(main)/layout.tsx.
The list page
Section titled “The list page”For a shopper with one list, find the list, then load it and render it with
ShoppingListDetail:
"use client";
import { useShoppingListScope } from "@evolve-storefront/feature-shopping-list/context";import { shoppingListDetailQueryOptions, shoppingListsQueryOptions,} from "@evolve-storefront/feature-shopping-list/queries";import { ShoppingListDetail, ShoppingListDetailEmpty, ShoppingListDetailSkeleton,} from "@evolve-storefront/feature-shopping-list/shopping-list-detail";import { useClientGqlFetcher } from "@labdigital/graphql-fetcher/react";import { useQuery } from "@tanstack/react-query";import type { ReactNode } from "react";
export function ShoppingList(): ReactNode { const gqlClient = useClientGqlFetcher(); const scope = useShoppingListScope(); const lists = useQuery(shoppingListsQueryOptions({ gqlClient, scope, enabled: true }));
if (lists.isLoading) return <ShoppingListDetailSkeleton />;
const listId = lists.data?.items[0]?.id; return listId ? <ListContents id={listId} /> : <ShoppingListDetailEmpty />;}
function ListContents({ id }: { id: string }): ReactNode { const gqlClient = useClientGqlFetcher(); const scope = useShoppingListScope(); const detail = useQuery(shoppingListDetailQueryOptions({ gqlClient, scope, shoppingListId: id }));
if (detail.isPending) return <ShoppingListDetailSkeleton />; if (!detail.data) return <ShoppingListDetailEmpty />;
return ( <ShoppingListDetail list={detail.data} renderItem={(item, api) => ( <div> <p>{item.variant.name}</p> <button type="button" onClick={api.remove} disabled={api.isRemoving}> Remove </button> </div> )} /> );}Without children, ShoppingListDetail renders its standard layout: the title,
the product count, the items with pagination, and the empty state. In
renderItem you write the markup, and api does the work: remove,
setQuantity, isRemoving and isUpdatingQuantity. The reference sites’
line item also has a quantity stepper and the cart’s CartAddProductButton.
For a different layout, pass children (ShoppingListDetailHeader,
ShoppingListDetailActions, ShoppingListDetailItems, …). ShoppingListDetailItems
then needs shoppingListId, items and renderItem. For an associate’s named
lists, use
ShoppingListOverview.
The reference B2C site’s shopping-lists/page.tsx shows both modes on one
route.
Check that it works
Section titled “Check that it works”-
pnpm codegenfinishes without errors. This means your supergraph serves every document the feature sends. -
The messages are complete. Run this in
frontend/site:pnpm evolve --package @evolve-storefront/commands messages build --checkThis writes nothing. It fails when the files would change, or when your source locale still has
[placeholder]values. -
pnpm tscpasses infrontend/site. -
Start the site and click the heart on a product card. A toast says the product was added, and the network panel shows
ShoppingListLineItemsAdd. A new visitor has no list yet, so the backend creates one with that product in it. -
Open the list page. The product is there, and Remove takes it out.
Next steps
Section titled “Next steps”- Customize a component
- Extend a feature’s data
- Style and brand
- The setup page of
feature-shopping-list, with everyShoppingListProviderprop

