# Loading data from unified GraphQL schema
URL: https://developers.front-commerce.com/docs/3.x/get-started/loading-data-from-unified-graphql-schema
{frontMatter.description}
## Prerequisites
Before diving into the code, ensure you have the following prerequisites in
place:
- A Front-Commerce project configured and ready to use
- Basic knowledge of [Remix routes](./your-first-route.mdx)
- [FAQ GraphQL Module](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/tree/main/skeleton/example-extensions/faq-demo/graphql)
installed and configured if you want to execute code samples without any
changes
## Loading Data
In your app directory, create a new route file (e.g.,
`app/routes/_main.faq.$slug.tsx`) and add the following code:
```tsx title="app/routes/_main.faq.$slug.tsx"
import { json } from "@front-commerce/remix/node";
import { useLoaderData } from "@front-commerce/remix/react";
import type { LoaderFunctionArgs } from "@remix-run/node";
import FaqDetail from "theme/pages/FaqDetail";
import { isRouteErrorResponse, useRouteError } from "@remix-run/react";
import { FaqEntryQueryDocument } from "~/graphql/graphql";
import { FrontCommerceApp } from "@front-commerce/remix";
// The route exports a `loader` function that is responsible
// for fetching data and throwing errors, ensuring that
// the main component is only rendered in the "happy path".
export const loader = async ({ context, params }: LoaderFunctionArgs) => {
const { slug } = params;
// The `loader` uses the `FrontCommerceApp` class to access the Front-Commerce
// core from Remix. In this example, we use it to fetch data from the GraphQL
// unified API (similar to the one you're used to).
const app = new FrontCommerceApp(context.frontCommerce);
const response = await app.graphql.query(FaqEntryQueryDocument, {
input: { slug },
});
if (!response.faqEntry) {
throw new Response("Question not found", { status: 404 });
}
return json({
faqEntry: response.faqEntry,
});
};
// The main component is a plain React component that receives
// the data from the loader, using Remix fetching primitives (`useLoaderData`)
// both on the server and on the client.
export default function Component() {
const { faqEntry } = useLoaderData();
return ;
}
// The route also exports an ErrorBoundary component that is responsible
// for displaying errors. It can be used to display a custom error page.
export const ErrorBoundary = () => {
const error = useRouteError();
if (isRouteErrorResponse(error)) {
return
FAQ : question not found
;
}
};
```
This is an example of a route that fetches a question from the GraphQL API and
renders it using a `` Page component (that is a plain React
component). As you can see, all the logic belongs to the route!
## Reference
As we can see here retrieving dynamic data from Front-Commerce unified GraphQL
schema can be done in a natural way from any route. But this only scratches the
surface of what you can do with Front-Commerce and Remix.
- [`loader`](https://remix.run/docs/en/main/route/loader) - _Each route can
define a "loader" function that provides data to the route when rendering._
- [`action`](https://remix.run/docs/en/main/route/action) - _A route `action` is
a server only function to handle data mutations and other actions. If a
non-GET request is made to your route (`DELETE`, `PATCH`, `POST`, or `PUT`)
then the action is called before the loaders.._
- [`Component`](https://remix.run/docs/en/main/route/component) - _The default
export of a route module defines the component that will render when the route
matches._
- [`ErrorBoundary`](https://remix.run/docs/en/main/route/error-boundary) - _A
Remix ErrorBoundary component works just like normal React
[error boundaries](https://reactjs.org/docs/error-boundaries.html), but with a
few extra capabilities._
- [`FrontCommerceApp`](../04-api-reference/front-commerce-remix/front-commerce-app.mdx) -
_The FrontCommerceApp class is the main entry point to the Front-Commerce
core. It provides access to the Front-Commerce core and to the GraphQL
client._
---
# Your first route
URL: https://developers.front-commerce.com/docs/3.x/get-started/your-first-route
{frontMatter.description}
# File-based routing
Front-Commerce uses Remix. Remix uses
[API Routes](https://remix.run/docs/en/main/guides/api-routes) to handle all the
routing of you application, and display the correct page when a user navigates
to a URL. All routes are located under `app/routes` folder. API Routes will use
the default export of each Route file to generate the route's UI. Go ahead and
create a new route `hello-world.tsx`, that will display content when users
browse http://localhost:4000/hello-world:
```tsx title="app/routes/hello-world.tsx"
export default function HelloWorld() {
return (
Hello world!
);
}
```
# Loaders
In order to display dynamic content in your routes, you will need to fetch some
data. This is done by using Remix's Routes API method `loader`.
Here is a basic example of how a `loader` could be used:
```tsx title="app/routes/hello-world.tsx"
import type { LoaderFunction } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
export const loader: LoaderFunction = () => {
// This method will be called prior to rendering your client-side route, it's generally used to fetch the data for your component(s)
return { name: "Jack Doe" };
};
export default function HelloWorld() {
const { name } = useLoaderData();
return (
Hello {name}!
);
}
```
# And many more features
Routes play a fundamental part in a Remix (and Front-Commerce) application. It
is the glue between the URL requested by a user and the response your
application will send. We will continue to learn how to use them for the most
common actions in the next sections, but here is a preview of everything a route
could do:
```tsx title="app/routes/hello-world.tsx"
import {
json,
type ActionFunction,
type HeadersFunction,
type LinksFunction,
type LoaderFunction,
type MetaFunction,
} from "@remix-run/node";
import { useLoaderData, Form, useRouteError } from "@remix-run/react";
export const headers: HeadersFunction = ({
actionHeaders,
loaderHeaders,
parentHeaders,
errorHeaders,
}) => ({
"X-User-Id": loaderHeaders.get("X-User-Id"),
"Cache-Control": "max-age=300, s-maxage=3600",
});
export const loader: LoaderFunction = async () => {
// This method will be called prior to rendering your client-side route, it's generally used to fetch the data for your component(s)
const user = await fetch("https://my-site.com/api/user").then((res) =>
res.json()
);
return json({ name: user.name }, { headers: { "X-User-Id": user.id } });
};
export const links: LinksFunction = () => {
return [
{
rel: "icon",
href: "/favicon.png",
type: "image/png",
},
{
rel: "stylesheet",
href: "https://my-site.com/styles.css",
},
{
rel: "preload",
href: "/images/banner.jpg",
as: "image",
},
];
};
export const action: ActionFunction = async ({ params, request }) => {
const data = await request.formData();
const email = data.get("email");
await fetch("https://my-site.com/api/contact-list", {
method: "POST",
body: JSON.stringify({ email }),
});
return { success: true };
};
export const meta: MetaFunction = ({ data }) => {
return [{ title: `Hello ${data.name}` }];
};
export default function HelloWorld() {
const { name } = useLoaderData();
return (
);
}
```
# External resources
- [Contact page example](https://remix.run/docs/en/main/start/tutorial#the-contact-route-ui)
- [API Routes documentation](https://remix.run/docs/en/main/guides/api-routes).
- [Loading data from an external API](https://daily-dev-tips.com/posts/remix-loading-data-from-an-external-api/)
- [Interactive Remix routing](https://interactive-remix-routing-v2.netlify.app/)
---
# Your first test 🧪
URL: https://developers.front-commerce.com/docs/3.x/get-started/your-first-test
{frontMatter.description}
# Resources
- [Vitest](https://vitest.dev/)
- [React Testing Library](https://testing-library.com/docs/react-testing-library/intro/)
- [@front-commerce/core/testing](../04-api-reference/front-commerce-core/testing.mdx#createfrontcommerceproviderstub)
- [@remix-run/testing](https://remix.run/docs/en/main/other-api/testing)
:::important
Tests defined in your custom extensions will only be tested if the extension has
been added to the `front-commerce.config.ts`
We match tests to the following patterns:
- `**/*.{test,spec}.?(c|m)[jt]s?(x)` - file based testing
- `**/__tests__/*.?(c|m)[jt]s?(x)` - folder based testing
:::
# Testing a Component
To test a component you can implement a test file with the following structure:
```tsx title="app/theme/components/MyComponent.test.tsx"
const MyComponent = () => {
const intl = useIntl();
return ;
};
describe("MyComponent", () => {
it("should render the Hello World text", () => {
render();
expect(screen.getByText("Hello World")).toBeInTheDocument();
});
});
```
You will notice at this stage the test will fail because the `useIntl` is
missing the context for the `IntlProvider`. To fix this, you can use the
[`FrontCommerceProviderStub`](../04-api-reference/front-commerce-core/testing.mdx#createfrontcommerceproviderstub)
to provide the context to the component.
```tsx title="app/theme/components/MyComponent.test.tsx"
import { createFrontCommerceProviderStub } from "@front-commerce/core/testing";
import { render, screen } from "@testing-library/react";
const FrontCommerceProviderStub = createFrontCommerceProviderStub({
messages: {
"my-button.label": "Hello World",
},
});
const MyComponent = () => {
const intl = useIntl();
return ;
};
describe("MyComponent", () => {
it("should render the Hello World text", () => {
render(
);
expect(screen.getByText("Hello World")).toBeInTheDocument();
});
});
```
# Testing a Route
To test a route you can implement a test file with the following structure:
:::caution
Do not place the test file in the `app/routes` directory. As this will be
detected as a nested route by Remix.
:::
```tsx title="app/__tests__/routes/hello-world.test.tsx"
import { json } from "@front-commerce/remix/node";
import { createRemixStub } from "@remix-run/testing";
import { render, screen, waitFor } from "@testing-library/react";
import { createFrontCommerceProviderStub } from "@front-commerce/core/testing";
import HelloWorldRoute from "../../routes/hello-world";
const FrontCommerceProviderStub = createFrontCommerceProviderStub();
describe("hello-world route", () => {
it("should render the component with fetched data", async () => {
// Define your mock data based on the expected GraphQL response structure
const mockData = {
shop: {
url: "https://example.com",
},
me: {
firstname: "John",
},
navigationMenu: [
{ id: "1", name: "Category 1", path: "/category-1" },
{ id: "2", name: "Category 2", path: "/category-2" },
{ id: "3", name: "Category 3", path: "/category-3" },
],
title: "Hello World",
};
const RemixStub = createRemixStub([
{
path: "/",
Component: () => (
),
loader: () => {
return json(mockData);
},
},
]);
render();
// Note that the rendering is asynchronous, so we need to wait for the component to be rendered
await waitFor(() => {
expect(screen.getByText(`Hi John đź‘‹`)).toBeInTheDocument();
expect(
screen.getByText(`Welcome to https://example.com`)
).toBeInTheDocument();
// Verify one of the navigation menu items is rendered
expect(screen.getByText("Category 1")).toBeInTheDocument();
});
});
});
```
---
# Add a component to Storybook
URL: https://developers.front-commerce.com/docs/3.x/guides/add-component-to-storybook
{frontMatter.description}
import Figure from "@site/src/components/Figure";
The concept lying behind a Design System is to document how your brand
communicates with its users. This goes from global guides such as "Voice and
tone", "Code Convention", "Accessibility"… to components documentation that
focus on where and how to use a specific component.
Such a Design System should be customized to the way your team(s) work and
should evolve according to your needs. But with Front-Commerce, we provide you a
base you can iterate on.
Technically, we use [Storybook](https://storybook.js.org/) which is a tool that
references `stories` for each component you want to document. With these
stories, it will launch for you a website that allows you to browse through your
components.
In this guide we will go through a basic tutorial that gets you started with
writing stories within Front-Commerce. But if you want to learn more about
Storybook, please refer to
[its documentation](https://storybook.js.org/basics/writing-stories/).
{/* TODO For more detailed information about Front-Commerce specific configurations of Storybook, please refer to TODO */}
## Add your own story
In order to add your own story, you need to create a new file, that will
document how to use a component and will reference each edge case for this
component. Each case will be what we call a `story`.
As explained in
[Storybook's documentation](https://storybook.js.org/basics/writing-stories/):
> A story captures the rendered state of a UI component.
To do this, you will create a `.stories.tsx` file next to your component. In
this example, we will use the same component as in Create a UI Component:
`IllustratedContent.tsx`. Hence, your stories file will be
`IllustratedContent.stories.tsx` and will look like this:
{/* TODO update : [Create a UI Component](./create-a-ui-component) */}
```tsx title="app/theme/components/molecules/IllustratedContent/IllustratedContent.stories.tsx"
import type { Meta, StoryObj } from "@storybook/react";
import IllustratedContent from "./IllustratedContent";
import Icon from "theme/components/atoms/Icon";
import { H3 } from "theme/components/atoms/Typography/Heading";
const meta: Meta = {
component: IllustratedContent,
};
export default meta;
type Story = StoryObj;
export const Default: Story = {
render: () => (
}>
Shipping within 48h
),
};
```
Thanks to this file, when you launch Storybook (`pnpm run styleguide`), it will
**register** the stories for the component `IllustratedContent`. It will make it
accessible in the Storybook's website under `app` > `theme` > `components` >
`molecules` \> `IllustratedContent`. And if you click on the `Default` story in
the navigation, it will display a default story which will show the standard
usecase of the `IllustratedContent` component.

:::note
As you might have noticed, once you have added the above `IllustratedContent`
story, if you edit the story or the component itself, it will update
automatically in Storybook (this feature is called hot reloading). It greatly
improves the experience of authoring UI components.
:::
For now, we've registered only one story for the `IllustratedContent`. But you
can add more to better explain edge cases of your component. For instance you
could add a story with a very long content or with an `` instead of an
`` in order to better see how it will behave.
```tsx title="app/theme/components/molecules/IllustratedContent/IllustratedContent.stories.tsx"
import type { Meta, StoryObj } from "@storybook/react";
import IllustratedContent from "./IllustratedContent";
import Icon from "theme/components/atoms/Icon";
import { H3 } from "theme/components/atoms/Typography/Heading";
// highlight-start
import { BodyParagraph } from "theme/components/atoms/Typography/Body";
import Image from "theme/components/atoms/Image";
// highlight-end
const meta: Meta = {
// highlight-next-line
title: "My App/Components/Molecules/IllustratedContent",
component: IllustratedContent,
};
export default meta;
type Story = StoryObj;
export const Default: Story = {
render: () => (
}>
Shipping within 48h
),
};
// highlight-start
export const WithContent: Story = {
name: "With a lot of content",
render: () => (
}>
Shipping within 48h
We are using many delivery services to let you choose what is best for
you!
),
};
export const WithImage: Story = {
name: "With an image",
render: () => (
}
>
Shipping within 48h
),
};
// highlight-end
```

This can be a nice and efficient way to show your designer how the
implementation will behave and ask for feedback to improve the User Experience.
:::info
You can see that we added a `title` key in the `meta` object. This is a way to
organize your stories in Storybook. It will create a new section in the
navigation with the title you provided. In this example, the
`IllustratedContent` stories will be displayed under `My App` > `Components` >
`Molecules` > `IllustratedContent`.
We recommend organizing your stories, especially because we will explain
something later that will require having an organized structure.
More information about the `title` key can be found in the
[Storybook's documentation](https://storybook.js.org/docs/writing-stories/naming-components-and-hierarchy).
:::
## Add a new usecase to an existing story
There are some cases where the story already exists but you want to add a new
edge case. This will usually happen when you override a core component of
Front-Commerce and you want to update its story to document the new use case.
In this example, we will consider that you have overridden and updated the
[`Money`](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/blob/cfddfe8b4e27e46c3afb6e77753f02b4ab07b37b/packages/theme-chocolatine/theme/components/atoms/Typography/Money/Money.tsx)
component (as explained in [Override a component](./override-a-component.mdx))
to add a new property `hasBorder`.
To add this story, you will need to override the `Money.stories.tsx` in the same
way you overrode the `Money.tsx`. This means that you should copy the existing
story from
[`node_modules/@front-commerce/theme-chocolatine/theme/components/atoms/Typography/Money/Money.stories.tsx`](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/blob/cfddfe8b4e27e46c3afb6e77753f02b4ab07b37b/packages/theme-chocolatine/theme/components/atoms/Typography/Money/Money.stories.tsx)
to `app/theme/components/atoms/Typography/Money/Money.stories.tsx`.
Once you have copied the file, you need to change the `title` key in the `meta`
object. In our example, you can either modify the base title (e.g.,
`title: "Components/Atoms/Typography/MoneyOverride",`) or change the
organization to fit your application (e.g.,
`title: "My App/Components/Atoms/Typography/Money",`).
```tsx title="app/theme/components/atoms/Typography/Money/Money.stories.tsx"
// ... existing code
const meta: Meta = {
// highlight-next-line
title: "My App/Components/Atoms/Typography/Money",
tags: ["autodocs"],
component: Money,
// ... existing code
};
```
Then, you can add override stories to document the new use case. In this
example, we will add the new `hasBorder` property to the `Money` component.
```tsx title="app/theme/components/atoms/Typography/Money/Money.stories.tsx"
// ... existing code
argTypes: {
"value.amount": {
control: "number",
},
"value.currency": {
control: "select",
options: currenciesAllowedList as any as CurrencyType[],
},
// highlight-start
hasBorder: {
control: "boolean",
},
// highlight-end
},
args: {
"value.amount": 19999,
"value.currency": currenciesAllowedList[0],
// highlight-next-line
hasBorder: false,
},
// ... existing code
```
## Create stories for complex components
{/* You now are able to customize your stories and document all your components. */}
{/* However, you may stumble into more complex issues such as internationalisation, */}
{/* routing and data fetching in some of your components. */}
{/* But it is your lucky day! With Front-Commerce's components, we have had the same */}
{/* issues and have created helpers that should make your life easier when */}
{/* documenting those complex components. */}
{/* These helpers are located in */}
{/* [`node_modules/front-commerce/src/web/storybook/addons`](https://gitlab.blackswift.cloud/front-commerce/front-commerce/tree/2.x/src/web/storybook/addons) */}
{/* and you can import them by using the alias `web/storybook/addons/...`. For now, */}
{/* we have four of them: */}
{/* - [`web/storybook/addons/apollo/ApolloDecorator`](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/blob/784684ce56cca69ca5c2e42d5d421a8c0b4bb9c3/src/web/storybook/addons/apollo/ApolloDecorator.js): */}
{/* mocks the data fetched by your components */}
{/* - [`web/storybook/addons/form/formDecorator`](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/blob/784684ce56cca69ca5c2e42d5d421a8c0b4bb9c3/src/web/storybook/addons/form/formDecorator.js): */}
{/* mocks a Form surrounding the input component you are documenting */}
{/* - [`web/storybook/addons/router/routerDecorator`](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/blob/784684ce56cca69ca5c2e42d5d421a8c0b4bb9c3/src/web/storybook/addons/router/routerDecorator.js): */}
{/* mocks the routing and the URLs of your application */}
{/* TODO document usage of each of these helpers */}
:::info WIP
This section is currently being migrated.
:::
## Display only the relevant stories to your Design System
If you run the styleguide for your own project, you may notice that
Front-Commerce comes with a lot of stories. This is what serves as documentation
Front-Commerce base theme's core components.
However, you might not use each one of them in your final theme, and some
stories might become irrelevant in your design system. To choose which ones to
display, you need to update the `.storybook/main.ts` configuration file, and add
the key
[`stories`](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/blob/cfddfe8b4e27e46c3afb6e77753f02b4ab07b37b/skeleton/.storybook/main.ts#L24).
```diff title=".storybook/main.ts"
# ...existing code
export default {
- stories: ["../**/*.stories.@(js|jsx|mjs|ts|tsx)", ...extensionThemeStories],
+ stories: ["../app/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
addons: [
# ...existing code
],
}
```
This configuration ensures that Storybook does not load unnecessary files,
improving performance and organization. Additionally, you can exclude certain
folders by adjusting the glob patterns.
## Automatic documentation
Storybook provides automatic documentation for your components. It can display
description, prop tables, usage examples and interactive controls automatically.
Here is the way to add automatic documentation to your stories based on our
`IllustratedContent` example:
```tsx title="app/theme/components/molecules/IllustratedContent/IllustratedContent.stories.tsx"
const meta: Meta = {
title: "My App/Components/Molecules/IllustratedContent",
component: IllustratedContent,
// highlight-start
tags: ["autodocs"],
parameters: {
docs: {
description: {
component: "A component that displays an image or an icon with text.",
},
},
},
// highlight-end
};
```
You can now see the documentation of your component in Storybook:
\*
For a more detailed documentation example, you can refer to the
[`Money`](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/blob/cfddfe8b4e27e46c3afb6e77753f02b4ab07b37b/packages/theme-chocolatine/theme/components/atoms/Typography/Money/Money.stories.tsx)
story example.
For further details, please check the
[official Storybook documentation](https://storybook.js.org/docs/writing-docs/autodocs).
---
# Using DataLoaders in GraphQL loaders
URL: https://developers.front-commerce.com/docs/3.x/guides/caching-remote-data/using-dataloaders-in-graphql-loaders
## What are DataLoaders?
DataLoader is a pattern promoted by Facebook, from their internal
implementations, to solve problems with data fetching. We use this name because
it is the name of the reference implementation in Javascript:
[graphql/dataloader](https://github.com/graphql/dataloader).
A DataLoader is instantiated with a **batching function**, which allows data to
be fetched in groups (see
[Batching](../../05-concepts/common-issues-in-the-data-fetching-layer.mdx#batching)).
It also has a caching strategy that prevents fetching the same data twice in the
same request or across requests (see
[Caching](../../05-concepts/common-issues-in-the-data-fetching-layer.mdx#caching)).
:::note
For a better understanding of why we use DataLoaders, read the
[Common issues in the data fetching layer](../../05-concepts/common-issues-in-the-data-fetching-layer.mdx)
guide.
:::
By default every DataLoader provides request-level caching. But this can be
configured to switch to a persistent caching strategy instead (see
[Configure caching strategies](./02-configure-caching-strategies.mdx#caching-strategies)).
We encourage you to read the
[DataLoader readme](https://github.com/graphql/dataloader/blob/main/README.md)
documentation to learn more about how it works.
Front-Commerce provides a factory function to create DataLoaders from your
GraphQL modules while keeping caching strategies configurable. Under the hood,
it is a pure DataLoader instance, so you could use it in a standard manner.
## Using DataLoaders in Front-Commerce
When building a GraphQL module, Front-Commerce will inject a `makeDataLoader`
factory function in your
[module’s `contextEnhancer` function](../../04-api-reference/front-commerce-core/graphql.mdx#contextenhancer-function).
### `makeDataLoader` usage
The `makeDataLoader` factory allows developers to build a DataLoader without
worrying about the current store scope (in a multi-store environment) or caching
concern.
Here is an example based on the use case above:
```ts title="extensions/acme/inventory/graphql/index.ts"
import { createGraphQLModule } from "@front-commerce/core/graphql";
export default createGraphQLModule({
namespace: "AcmeInventory",
loadRuntime: () => import("./runtime"),
typeDefs: /* GraphQL */ `
extend type Product {
qty: Int
}
`,
});
```
```ts title="extensions/acme/inventory/graphql/runtime.ts"
import { createGraphQLRuntime } from "@front-commerce/core/graphql";
import StockLoader from "./loader";
import axios from "axios";
export default createGraphQLRuntime({
contextEnhancer: ({ makeDataLoader, config }) => {
const axiosInstance = axios.create({
baseURL: config.inventoryApiEndpointUrl,
});
return {
// create an instance of the loader, to be made available in resolvers
Stock: new StockLoader(makeDataLoader, axiosInstance),
};
},
resolvers: {
Product: {
qty: ({ sku }, _, { loaders }) => {
// use the loader instance to fetch data
// batching and caching is transparent in the resolver
return loaders.Stock.loadBySku(sku);
},
},
},
});
```
```ts title="extensions/acme/inventory/graphql/loader.ts"
import { reorderForIds } from "@front-commerce/core/graphql";
import type { DataLoaderScopedFactory } from "@front-commerce/core/cache";
import type { AxiosInstance } from "axios";
class StockLoader {
private httpClient: AxiosInstance;
private dataLoader: ReturnType>;
constructor(
makeDataLoader: DataLoaderScopedFactory,
httpClient: AxiosInstance
) {
// The `Stock` key here must be unique across the project
// and is used in cache configuration to determine the caching strategy to use
this.dataLoader = makeDataLoader("Stock")(
(skus: readonly (string | number)[]) => this.loadStocksBatch(skus)
);
this.httpClient = httpClient;
}
// Our batching function that will be injected in the DataLoader factory
// it is important to return results in the same order than the passed `skus`
// hence the use of `reorderForIds` (documented later in this page)
private loadStocksBatch(skus: readonly (string | number)[]) {
return this.httpClient
.get("/stocks", { params: { skus } })
.then((response) => response.data.items)
.then(reorderForIds(skus, "sku"));
}
loadBySku(sku: string | number) {
return this.dataLoader.load(sku);
}
}
export default StockLoader;
```
The 2nd parameter to `makeDataLoader` are the options to pass to the DataLoader
instance. You usually don't have to use it. Please refer to
[dataloader's documentation](https://github.com/graphql/dataloader#new-dataloaderbatchloadfn--options)
for further information.
### Useful patterns
#### Prevent caching errors (data not found)
Batching functions will sometimes return `null` or _falsy_ data for nonexistent
items. By default, these values will be cached so further data retrieval could
return this `null` value instead of doing a remote API call.
In some specific cases, you may want to force fetching data from the remote
source every time. You can do so by returning an `Error` for the nonexistent
item.
Here is an example:
```js
const fooLoader = makeDataLoader("AcmeFoo")((ids) =>
loadFooBatch(ids).then((items) =>
items.map((item) => {
if (!item) {
return new Error("not found");
}
return item;
})
)
);
```
#### Using a predefined TTL
In some contexts, cache invalidation could be impossible or difficult to
implement in remote systems. You may still want to leverage Front-Commerce's
caching features, such as
[the Redis persistent cache](../../04-api-reference/front-commerce-core/caching-strategies.mdx#redis),
to improve performance of your application.
The Redis strategy supports an additional option (to be provided during
instantiation) that allows you to create a loader with a specified expiration
time for cached items.
Here is how you could use it:
```js
const fooLoader = makeDataLoader("AcmeFoo")(
(ids) => loadFooBatch(ids),
{ expire: EXPIRE_TIME_IN_SECONDS } // see https://github.com/DubFriend/redis-dataloader
);
```
#### Caching scalar values
DataLoaders mostly manipulate objects. Hence, it is safer to design your
application to return objects from batching functions. This will ensure a wider
range of caching strategies' compatibility (ex: Redis strategy does not support
caching of scalar values).
```js
const fooLoader = makeDataLoader("AcmeFoo")((ids) =>
loadFooBatch(ids).then((results) =>
results.map((result) => ({ value: result }))
)
);
// …
return fooLoader.load(id).then((data) => data.value);
```
## Helpers available to create dataLoaders
Writing batching functions and loaders could lead to reusing the same patterns.
We have extracted some utility functions to help you in this task.
You can find them in the
[`node_modules/@front-commerce/core/graphql/dataloaderHelpers.ts`](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/blob/507d58e1d70368cab9a0ff7b93f11af374823f68/packages/core/graphql/dataloaderHelpers.ts)
module.
### `reorderForIds`
Batch functions must satisfy two constraints to be used in a DataLoader (from
the
[graphql/dataloader documentation](https://github.com/graphql/dataloader#batch-function)):
1. The Array of values must be the same length as the Array of keys
2. Each index in the Array of values must correspond to the same index in the
Array of keys.
`reorderForIds` will ensure that these constraints are satisfied.
Signature: `const reorderForIds = (ids, idKey = "id") => data => sortedData;`
It will sort `data` by `idKey` to match the order from the `ids` array passed in
parameters. In case no matching values is found, it will return `null` and log a
message so you could then understand why no result was found for a given id.
Example:
```js
// skus will very likely be a param of your batch loader
const skus = ["P01", "P02", "P03"];
return (
axiosInstance
.get("/frontcommerce/price", {
params: {
skus: skus.join(","),
},
})
.then((response) => {
const prices = response.data;
/* [
{sku: "P02", price: 12},
{sku: "P03", price: 13},
{sku: "P01", price: 11},
] */
return prices;
})
// results will be sorted according to the initial skus passed (P01, P02, P03)
.then(reorderForIds(skus, "sku"))
);
```
### `reorderForIdsCaseInsensitive`
As its name implies, it is very similar to `reorderForIds` but ids are compared
in a case insensitive way.
Example:
```js
return axiosInstance
.get(`/products`, { params: searchCriteria })
.then((response) => response.data.items.map(convertMagentoProductForFront))
.then(reorderForIdsCaseInsensitive(skus, "sku"));
```
### `makeBatchLoaderFromSingleFetch`
Until now, we created batching functions using a remote API that allowed to
request several results at once
(`https://inventory.example.com/stocks?skus=PANT-01,PANT-02,…,PANT-10`).
When using 3rd party APIs or legacy systems, such APIs might not always be
available. Using dataLoaders in this case will not allow you to reduce the
number of requests in the absolute, however it could still allow you to prevent
most of these requests (or reduce its number in practice) thanks to caching. It
is thus very convenient when dealing with a slow service.
The `makeBatchLoaderFromSingleFetch` allows you to create a batching function
from a single fetching function easily.
Pseudo signature:
```js
makeBatchLoaderFromSingleFetch = (
function singleFetch, // function that fetches data for a single id
function singleResponseMapper = ({ data }) => data // function that transform a response into data
) => ids => Observable(sortedData);
```
Example (from
[the Magento2 category loader](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/blob/6ba7ceed3244c47f1c75e03a60c8a5e87a3f5104/src/server/modules/magento1/catalog/categories/categoryLoader.js#L6)):
```js
import { makeBatchLoaderFromSingleFetch } from "server/core/graphql/dataloaderHelpers";
// …
const loadBatch = makeBatchLoaderFromSingleFetch(
(id) => axiosInstance.get(`/categories/${id}`),
(response) => convertCategoryMainAttributesForFront(response.data)
);
const loader = makeDataLoader("CatalogCategory")(
(ids) => loadBatch(ids).toPromise() // <-- note the `toPromise()` here
);
```
:::note
`makeBatchLoaderFromSingleFetch` returns an Observable. You must thus convert it
to a Promise using the `.toPromise()` method.
:::
---
# Change a resolver behavior
URL: https://developers.front-commerce.com/docs/3.x/guides/change-resolver-behavior
{frontMatter.description}
As an example, we will change the way the Product `meta_description` field value
is generated.
## Create dedicated GraphQL module
First, you have to create a GraphQL module. For that, you can follow the process
detailed in
[Extend the GraphQL schema](./extend-the-graphql-schema#add-a-graphql-module-within-the-extension).
In this example, we have a base GraphQL module for `ProductMetaDescription`
which does not do anything yet:
```ts title="extensions/acme-extension/modules/productmetadescription/index.ts"
import { createGraphQLModule } from "@front-commerce/core/graphql";
export default createGraphQLModule({
namespace: "ProductMetaDescription",
});
```
## Set the module dependency
Before being able to inject your custom resolver logic, you first need to find
the module that defines the resolver for the field. In our example, the Product
`meta_description` is resolved
[by the resolver provided by the `Magento2/Catalog/Products` module](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/blob/23937b329c11d5a6f7a0e9e631b9ffffeca16bf5/packages/magento2/modules/catalog/products/resolvers.js#L346).
As a result, the `Magento2/Catalog/Products` module must be added as a
dependency of our custom module:
```ts title="extensions/acme-extension/modules/productmetadescription/index.ts"
import { createGraphQLModule } from "@front-commerce/core/graphql";
export default createGraphQLModule({
namespace: "ProductMetaDescription",
// add-next-line
dependencies: ["Magento2/Catalog/Products"],
});
```
## Implement your custom resolver logic
Our custom module can now provide a resolver with a custom logic in your
extension's runtime.
```ts title="extensions/acme-extension/modules/productmetadescription/runtime.ts"
export default createGraphQLRuntime({
resolvers: {
Product: {
meta_description: (product) => {
// your implementation here
return "my custom description";
},
},
},
});
```
For this resolver to be taken into account, you will need to include the
`GraphQL Runtime` import in your `GraphQL Module`:
```ts
import { createGraphQLModule } from "@front-commerce/core/graphql";
export default createGraphQLModule({
namespace: "ProductMetaDescription",
dependencies: ["Magento2/Catalog/Products"],
// add-next-line
runtime: () => import("./runtime"),
});
```
This custom resolver will now be merged with the existing ones and the
`meta_description` will be resolved with our custom implementation instead of
the default one.
---
# Adding a configuration provider
URL: https://developers.front-commerce.com/docs/3.x/guides/configuration/adding-a-configuration-provider
{frontMatter.description}
## What is a configuration provider?
The goal is to give access to a configuration object that contains all the
configuration of the store. This configuration object can contain any kind of
configuration. It can be flags about a feature activation, some credentials to
connect to a remote service, etc.
However, not all applications will need every single configuration. For
instance, a shop that chose to use [Algolia](https://www.algolia.com/) won't
have the same configurations than a shop that chose
[Elasticsearch](https://www.elastic.co/) for product search. The goal of the
configuration providers is to define the configurations needed for the specific
modules you use in your application.
On server start, the configuration providers will then be combined to create the
global configuration object. This configuration object will then be available on
each server request.
This can be illustrated by starting your Front-Commerce server in development
mode. If your server is running on `http://localhost:3000` and you open the URL
[`/__front-commerce/debug`](http://localhost:3000/__front-commerce/debug), you
will have a dump of the configuration for this specific request under the
section `config`.
```json
{
"allShops": {
"store:default": {
"id": "default",
"url": "http://localhost:3000",
"locale": "en-GB",
"magentoStoreCode": "default",
"currency": "EUR"
},
"store:fr": {
"id": "fr",
"url": "http://fr.localhost:3000",
"locale": "fr-FR",
"magentoStoreCode": "fr",
"currency": "EUR"
}
},
"currentShopId": "default",
"shop": {
"id": "default",
"url": "http://localhost:3000",
"locale": "en-GB",
"magentoStoreCode": "default",
"currency": "EUR"
},
"cache": {
"defaultMaxBatchSize": 100,
"strategies": [
{
"implementation": "Redis",
"supports": "*",
"disabledFor": [],
"config": {
"host": "localhost"
}
}
]
},
"contentSecurityPolicy": {
"__dangerouslyDisable": false,
"directives": {
"scriptSrc": [],
"frameSrc": [],
"styleSrc": [],
"imgSrc": [],
"fontSrc": [],
"connectSrc": [],
"frameAncestors": [],
"baseUri": []
},
"reportOnly": {
"scriptSrc": false,
"frameSrc": false,
"styleSrc": false,
"imgSrc": false,
"fontSrc": false,
"frameAncestors": false,
"connectSrc": false
}
},
"cors": {
"origin": {
"referal-0": "http://magento23.test"
}
},
"device": {
"memoizationMaxAge": 60000,
"type": "pc",
"viewportWidthInPx": 1400
},
"public": {
"analytics": {
"disableDevWarning": false
},
"deprecations": {
"ignore": "",
"trace": ""
},
"compatEnv": {
"FRONT_COMMERCE_WEB_DEV_ANALYTICS_WARNING_DISABLE": "true"
},
"password": {
"disableHint": false
}
"shop":{
"id": "default",
"url": "http://localhost:3000",
"locale": "en-GB",
"currency": "EUR"
}
},
"magento": {
"endpoint": "http://magento23.test",
"version": "2",
"proxiedPaths": []
}
}
```
:::tip
When the `FRONT_COMMERCE_ENV` environment is not set to `production`, for
example in a staging environment, the configuration will still be available in
the `__front-commerce/debug` page, but the page will be secured with a token
configurable with the `FRONT_COMMERCE_DEBUG_TOKEN` environment variable.
Then you can access the endpoint with the token in the URL:
`/__front-commerce/debug?token=your-token`.
:::
## Configuration provider definition
In practice, a configuration provider is an object with five properties: a
`name`, a `schema`, static `values` (available independently from the request),
a `slowValuesOnEachRequest` function to resolve values depending on the request
and a `fetchRemoteConfiguration` function to fetch values remotely.
```js
const serviceConfigurationProvider = {
name,
schema,
values,
slowValuesOnEachRequest,
fetchRemoteConfiguration,
};
```
See the sections below to understand what each key stands for.
export function Label({ children }) {
return (
{children}
);
}
### `name`
The identifier of the configuration provider. This is needed for configuration
providers' registration. See
[Inject a configuration provider](#inject-a-configuration-provider) for more
details.
```js
const name = "serviceProvider";
```
### `schema`
It's a function returning an object representing the definition of the fields
that will appear in the global configuration object if the configuration
provider is registered. It can define things like field formats, default values
or environment variables.
The schema definition is based on
[convict](https://github.com/mozilla/node-convict), a library developed by
Mozilla to validate configurations. For a single field, the schema will have
these keys:
- `doc` (string): A description of the field and pointers to learn how to get
its value if it requires a manual operation
- `format` (string, array or function): The formatter used for this field's
value. See
[how validation works in convict](https://github.com/mozilla/node-convict#validation).
- `default` (any): A default value. If you don't have a default value, you still
have to set one by using the `null`. If there is no default value, it won't be
considered as a field definition.
- `env` (string, optional): The name of the environment variable that should be
used as the value. If none is given, the configuration won't be configurable
by environment. Please keep in mind that environment variables should still
match
[Front-Commerce's naming convention](/docs/2.x/reference/environment-variables#add-your-own-environment-variables).
Thus, if we want to define a configuration named `serviceKey` available in
`req.config.serviceKey` that would take its value from the environment variable
`FRONT_COMMERCE_SERVICE_KEY`, we would use this schema definition:
```js
const schema = () => ({
serviceKey: {
doc: "The key to get access to our remote service",
format: String,
default: null,
env: "FRONT_COMMERCE_SERVICE_KEY",
},
});
```
A configuration provider's schema is not limited to a single field though. You
can set multiple configuration keys but also nest deeper objects. For instance,
if we want to group a `key` and a `secret` into a single `service` key, we would
write the following schema:
```js
const schema = () => ({
service: {
key: {
doc: "The key to get access to our remote service",
format: String,
default: null,
env: "FRONT_COMMERCE_SERVICE_KEY",
},
secret: {
doc: "The secret to get access to our remote service",
format: String,
default: null,
env: "FRONT_COMMERCE_SERVICE_SECRET",
},
},
});
```
You could then access the values with `req.config.service.key` or
`req.config.service.secret`.
Please note that if another configuration provider's schema also defined a
`service` key, it would merge the definitions and in your final
`req.config.service` you would have both the keys from the other configuration
provider's schema and from the above schema.
### `values`
Most of the time `default` values in the schema is sufficient. However, in some
cases you may need to fetch some values from an API, a dynamic file, etc. This
is what `values` is for.
It is an optional promise that should return the missing values in your schema.
It will override default values and env values from the schema with the new
values. However only keys from the `values` result will take precedence, the
default values and env values will be kept for other keys. For instance, if we
implemented the following `values` promise, the `secret` would still be
`process.env.FRONT_COMMERCE_SERVICE_SECRET` from the above schema.
```js
const values = fetch("https://api.example.com/my-service-key")
.then((response) => response.json())
.then((key) => ({
service: {
key: key,
},
}));
```
Please note that this promise is launched only once on server bootstrap. If the
configuration changes over time on your API, the `req.config.service.key` value
will still be the same.
If it needs to change over time, please use `slowValuesOnEachRequest` or
`fetchRemoteConfiguration` instead.
### `slowValuesOnEachRequest`
:::note
`slowValuesOnEachRequest` is a low level API in Front-Commerce. You shouldn't be
need it in most cases.
:::
`slowValuesOnEachRequest` is a function that extracts configuration values from
the current request. For instance, depending on the URL, the configuration
`currentShopId` will get a different id and thus display different information.
```js
const slowValuesOnEachRequest = (req) => {
const url = req.originalUrl;
const shopId = getShopIdFromUrl(url);
return {
currentShopId: shopId,
};
};
```
:::caution
This part is named `slowValuesOnEachRequest` because we want to stress the fact
that it can have a huge impact on your website performance. Avoid its usage as
much as you can and try to use `values` instead. If this is the only way to
setup your configuration make sure to memoize its result to limit the
performance impact as much as possible.
```js
import memoize from "lodash/memoize";
const getShopIdFromHostname = memoize((hostname) => {
/* The definition of the `currentShopId` variable should live here */
return {
currentShopId: currentShopId,
};
});
const slowValuesOnEachRequest = (req) => {
const hostname = req.hostname;
return getShopIdFromHostname(req.hostname);
};
```
:::
### `fetchRemoteConfiguration`
:::note
`fetchRemoteConfiguration` is a low level API in Front-Commerce. You shouldn't
need it in most cases.
:::
`fetchRemoteConfiguration` is a function that is called at a later stage in
Front-Commerce request handling cycle to receive a fully initialized `request`
object, so that, for instance,
[you can instantiate a loader to retrieve some configuration from Magento](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/commit/f57ccabae32ebe6243fc213485078cb1f98f8a30#9e3c152a167c50222b152728db1b5946d409ba89_0_29).
:::caution
Like `slowValuesOnEachRequest`, `fetchRemoteConfiguration` can have a huge
impact on the performances. If you really need to implement it, please make sure
to memoize its result to limit the performance impact as much as possible.
:::
## Inject a configuration provider
Assuming that we have defined the following configuration provider:
```ts title="./config-providers/acmeConfigProvider.ts"
export default {
name: "acme-config",
schema: () => ({
service: {
key: {
doc: "The key to get access to our remote service",
format: String,
default: null,
env: "FRONT_COMMERCE_SERVICE_KEY",
},
secret: {
doc: "The secret to get access to our remote service",
format: String,
default: null,
env: "FRONT_COMMERCE_SERVICE_SECRET",
},
},
}),
};
```
### From your application configuration
You can add the configuration to your
[`front-commerce.config.ts`](../../04-api-reference/front-commerce-core/config.mdx)
file.
```ts title="front-commerce.config.ts"
import { defineConfig } from "@front-commerce/core/config";
import themeChocolatine from "@front-commerce/theme-chocolatine";
// add-next-line
import acmeConfigProvider from "./config-providers/acmeConfigProvider";
export default defineConfig({
extensions: [themeChocolatine()],
// add-start
configuration: {
providers: [acmeConfigProvider],
},
// add-end
});
```
### From an extension
Alternatively, you can add a configuration provider from an extension in the
[extensions definition](../../04-api-reference/front-commerce-core/defineExtension.mdx).
```ts title="./acme-extension.ts"
import { defineExtension } from "@front-commerce/core";
// add-next-line
import acmeConfigProvider from "./config-providers/acmeConfigProvider";
export default defineExtension({
name: "acme-extension",
// add-start
configuration: {
providers: [acmeConfigProvider],
},
// add-end
});
```
:::info
You can learn more about extensions in the
[Register an extensions](../register-an-extension.mdx) guide.
:::
---
# Create a Business Component
URL: https://developers.front-commerce.com/docs/3.x/guides/create-a-business-component
{frontMatter.description}
In Front-Commerce we have separated our components in two categories: the
[**UI** components](./create-a-ui-component) available in the
`app/theme/components` folder, and the **Business** components available in the
`app/theme/modules` and `app/theme/pages` folders.
:::note
If you would like to understand why we went for this organization, feel free to
refer to [React components structure](../concepts/react-components-structure/)
first.
:::
## What is a Business component
A Business component will be located in the `app/theme/modules` folder. Those
components are not meant to be reused a lot in your application, they are built
with a **very specific use in mind**. When creating a custom theme, they should
emerge in [your Pages components](../get-started/your-first-route/).
To illustrate this, imagine that you are building the homepage for your store.
You add a big hero image on top, some product listings for news and sales, a
reinsurance banner, etc.
Quickly, you will want to **extract** some components from your page to avoid a
_big bloated file_. Some of these components will be extracted as **reusable UI
components** but some are very specific to your page and there is no reason to
put them in the [`components`](./create-a-ui-component) folder.
:::note
They are often a mix between UI components and custom layout. They may be split
into multiple components if they are big enough.
:::
Generally, they are related to **your business** and often need backend data
like CMS content or store information. We refer to them as **Business
components** or even **modules**.
:::note
Unlike UI components, Business components are often _smart_ and contain logic.
We try to extract this logic in **hooks**, but more on that later.
:::
## Creating a store locator
To explain the concept and the emergence of modules, we will add a **store
locator** to our home page and see how to extract it properly as a module.
In the following steps, we are going to build our store locator and we will go
through:
1. Displaying a map on the homepage
2. Fetching the position of the store from the backend
3. Link both to have an actual module
### Installing the dependencies
To create the map, we are going to use the
[**react-leaflet**](https://react-leaflet.js.org/) package. It provides a
component that uses leaflet under the hood. It will allow us to display the
position of our store within
[OpenStreetMap](https://www.openstreetmap.org/search?query=Toulouse#map=12/43.6007/1.4329).
This is one of the biggest advantages of using React to build our front-end, we
have access to this huge ecosystem.
Let's install the required packages (versions are important):
```shell
pnpm install leaflet@^1.7 react-leaflet@3.2.5
```
:::note
Front-Commerce also [provides a `
### Our new Homepage
#### Override the default Home page
Before starting to edit the Home page, you first need to extend the theme. If
you don't have a module yet, please refer to
[Extend the theme](./override-a-component/#understanding-theme-overrides). Once
you have one, the goal will be to override the Home component from
[`node_modules/@front-commerce/theme-chocolatine/theme/pages/Home/Home.js`](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/blob/85bb712e7fbb0b2e74a6db643f5522451a4bdc58/packages/theme-chocolatine/theme/pages/Home/Home.jsx)
to `app/theme/pages/Home/Home.js`.
:::warning
Do not forget to restart your application (`pnpm start`) in case the override
does not work.
:::
#### Customize it
Once you have your own `Home` component in your module, it is time to customize
it and add your store locator.
You don't need to anticipate every **UI** or **Business** component in your
application. Only extract them when your component gets bigger or if you feel
the need to extract some of them.
To illustrate this point, we are going to create the first version of our map
into the homepage directly. We will start with hardcoded values for the store
coordinates. Then we will extract the store locator feature in its own module
component. And finally, we will fetch the actual coordinates from the **GraphQL
schema**.
The first working version will look like:
```tsx title="app/theme/pages/Home/Home.tsx"
// ... Existing imports
import { MapContainer, TileLayer, Marker, Popup } from "react-leaflet";
import "leaflet/dist/leaflet.css";
import "leaflet/dist/images/marker-icon.png";
import "leaflet/dist/images/marker-shadow.png";
const Home = () => (
{/* ... The rest of the homepage */}
My awesome store is HERE!
);
// ...
```
With that, you should see the map appear in your homepage.
{/* TODO: Uncomment when the CSP article is written */}
{/* :::warning Important */}
{/* You will detect issues with remote assets (images, CSS…) not being loaded. It is */}
{/* related to the default CSP headers */}
{/* sent by Front-Commerce. To allow new domains, you should modify your */}
{/* [`config/website.js`'s `contentSecurityPolicy` key](/docs/2.x/reference/content-security-policy) */}
{/* (i.e: define `imgSrc: ["*.openstreetmap.org"]`). */}
{/* ::: */}
### Extracting our new component
If the `Home` component grows too complex with multiple components, maintaining
it becomes challenging. At that point, it's best to extract the Store Locator
into its own module for better organization and maintainability.
To do so, we will reuse the exact same component but move it into its own module
in `app/theme/modules`.
```tsx title="app/theme/modules/StoreLocator/StoreLocator.tsx"
import React from "react";
import PropTypes from "prop-types";
import { MapContainer, TileLayer, Marker, Popup } from "react-leaflet";
import "leaflet/dist/leaflet.css";
import "leaflet/dist/images/marker-icon.png";
import "leaflet/dist/images/marker-shadow.png";
const StoreLocator = () => {
return (
My awesome store is HERE!
);
};
export default StoreLocator;
```
In order to make it consistent with other components in the application, we will
add two more files:
- `app/theme/modules/StoreLocator/index.ts`: will proxy the `StoreLocator.tsx`
file in order to be able to do imports on the folder directly. See
[this blog post](http://bradfrost.com/blog/post/this-or-that-component-names-index-js-or-component-js/)
for more context about this pattern.
```ts title="app/theme/modules/StoreLocator/index.ts"
export { default } from "./StoreLocator";
```
- `app/theme/modules/StoreLocator/StoreLocator.stories.tsx`: will add a story to
the Storybook of your application. The story will serve as living
documentation that will allow anyone to understand what the StoreLocator is
used for and how to use it.
```tsx title="app/theme/modules/StoreLocator/StoreLocator.stories.tsx"
import StoreLocator from "./StoreLocator";
import type { Meta, StoryObj } from "@storybook/react";
const meta: Meta = {
component: StoreLocator,
};
export default meta;
type Story = StoryObj;
export const Default: Story = {
args: {},
};
```
:::note
We won't focus on the story in this guide. But you can refer to the
[Storybook guide](./add-component-to-storybook) to learn how to
:::
### Fetching our data
Hardcoded values are perfectly fine. But if the coordinates change over time, it
might be a good idea to fetch them dynamically. This is what we will do in this
example.
Usually, the recommended way to retrieve data in components is to retrieve them
from the routes directly, and pass them as `props` to components. However when
not possible do to so, or in the case of client-side components like our
`StoreLocator`, we can retrieve dynamic data using API routes and fetchers
instead.
Thus, to fetch our data from GraphQL, we are going to create an **Hook** for our
store locator, which will be responsible of fetching the data from a custom API
route and transforming the store information to match our needs. This hook will
contain the **Business logic** of our component.
#### Creating the hook
```ts title="app/theme/modules/StoreLocator/useStoreLocator.ts"
import { useApiFetcher } from "@front-commerce/remix/react";
import { loader } from "routes/api.store-locator";
export default function useStoreLocator() {
const fetcher = useApiFetcher("/api/store-locator");
const { data, state } = fetcher.load();
return {
loading: state === "loading",
error: data?.error,
store: data?.store,
};
}
```
Here, we are using the
[`useApiFetcher`](../api-reference/front-commerce-remix/#useapifetcher) hook
which allows us to fetch data from an API route.
#### Implementing the API route
We will now create the API route that will be used to fetch the data.
```ts title="app/routes/api.store-locator.ts"
import { json } from "@front-commerce/remix/node";
import { FrontCommerceApp } from "@front-commerce/remix";
import { StoreLocatorQueryDocument } from "~/graphql/graphql";
export const loader = async ({ context }) => {
const app = new FrontCommerceApp(context.frontCommerce);
const store = await app.graphql.query(StoreLocatorQueryDocument);
return json({ store });
};
```
As you can see, our route needs a **Query** (`StoreLocatorQueryDocument`). This
is a `.gql` file that uses the **GraphQL** syntax. In our case, it will look
like:
```graphql title="app/theme/modules/StoreLocator/StoreLocatorQuery.gql"
query StoreLocator {
store {
name
phone
owner {
email
}
coordinates {
longitude
latitude
}
}
}
```
:::info
Front-Commerce will automatically generate the `StoreLocatorQueryDocument` from
the `.gql` file when starting the application. You can find it in the
`~/graphql/graphql.ts` file. Since our useApiFetcher is typed based on the route
loader, it has the benefit of automatically typing the whole hook based on the
GraphQL query.
:::
To better understand and test your schema, you can use **GraphQL Playground**.
It is a web interface for GraphQL, similar to what PhpMyAdmin is for MySQL.
### Making it dynamic
Now that we have our hook ready, we are going to use it in our store locator.
When dealing with asynchronous resources like fetching data from the backend,
you have to handle the loading state and error state. Here we we will show a
simple message such as "Loading..." or "Oops, an error occurred." to the user.
But in a real life application, you would want to show better messages depending
on your context.
:::info
For error handling, you could take a look at
[Error Boundaries](./error-handling-for-routes).
:::
```tsx title="app/theme/modules/StoreLocator/StoreLocator.tsx"
import React from "react";
import { MapContainer, TileLayer, Marker, Popup } from "react-leaflet";
import "leaflet/dist/leaflet.css";
import "leaflet/dist/images/marker-icon.png";
import "leaflet/dist/images/marker-shadow.png";
import useStoreLocator from "./useStoreLocator";
const StoreLocator = () => {
const { loading, error, store } = useStoreLocator();
if (loading) {
return
My awesome store ─ {props.store.name}
Email: {props.store.owner.email}
Phone: {props.store.phone}
);
};
export default StoreLocator;
```
## Using it in our App
We now have extracted all the store locator logic. We can now use our brand new
and shiny module component within the homepage.
```tsx title="app/theme/pages/Home/Home.tsx"
import StoreLocator from "theme/modules/StoreLocator";
const Home = () => (
{/* ... */}
);
```
As you can see, we did not use a relative import. This is because in
Front-Commerce we have a few aliases that will let you import files without
worrying about your current position in the folder structure.
In our case, the `Home` component being in `app/theme/pages/Home/Home.js`, we do
not have to import the `StoreLocator` by using relative paths
`../../modules/StoreLocator` but we can remain with
`app/theme/modules/StoreLocator` which is more explicit. This is possible for
any file located in the folder `theme/theme` of an extension.
And it works! You now have a clean `Home` page component that uses a Business
component which could be used anywhere in your application (About us, Contact,
etc.).
:::info Going further
The store locator we just created is very simple and it has a very limited
Business impact. The store locator module does not need to know the
implementation of the map itself (like the fact of using `react-leaflet`). So a
map component could be extracted in a **UI** component. But for the sake of this
guide, we kept it simple.
:::
As a final note, please keep in mind that splitting code is a difficult task. It
needs practice and refinement. But it is also a pretty personal point of view.
Thus one team could split code differently. In this guide we have made a choice
but feel free to make yours.
In any case, we advice you to not overcomplicate things and find a method that
matches your needs.
---
# Dependency Injection
URL: https://developers.front-commerce.com/docs/3.x/guides/dependency-injection
{frontMatter.description}
## Implementation
### Creating the Dependency
To create a dependency, define a function that initializes and returns the
dependency. For instance, `createAcmeDI` might create a client using the
`@acme/lib` library configured with specific settings.
```ts title="./extensions/acme/di.ts"
import type { ComputedConfig } from "@front-commerce/core/config";
import createClient from "@acme/lib";
export const createAcmeDI = (config: ComputedConfig["acme"]) => {
return {
client: createClient(config.api.url, config.api.token),
};
};
```
### Registering Dependencies
Dependencies should be registered within the extension definition, typically
inside the `onServerServicesInit` lifecycle hook. This is where you can define
what services or objects should be available for injection throughout your
application.
Example of registering an `acme` dependency:
```ts title="./extensions/acme/index.ts"
export default defineRemixExtension({
name: "acme-extension",
meta: import.meta,
unstable_lifecycleHooks: {
onServerServicesInit: async (services, request, config) => {
services.DI.register("acme", createAcmeDI(config.acme));
},
},
});
```
### Extending the type declarations
For TypeScript support, you'll need to extend the `DependencyInjection`
interface from `@front-commerce/types` to include your custom dependencies.
First ensure you have the `@front-commerce/types` package installed in your
project, as it provides the necessary types for DI.
```bash
pnpm add -D @front-commerce/types
```
Then the declaration can be placed in a `dependency-injection.d.ts` file within
your extension's types directory.
```ts title="./extensions/acme/types/dependency-injection.d.ts"
import type { createAcmeDI } from "../di";
declare module "@front-commerce/types" {
export interface DependencyInjection {
acme: ReturnType;
}
}
```
Make sure this types declaration file is included in your `tsconfig.json` file
under the `include` or include sections to ensure TypeScript is aware of these
custom types.
```json title="tsconfig.json"
{
// remove-next-line
"include": ["types/**/*"]
// add-next-line
"include": ["extensions/**/types/**/*", "types/**/*"]
}
```
:::tip
The `DependencyInjection` interface can be extended multiple times across your
extensions. This allows you to define dependencies in separate files and keep
your codebase organized.
:::
## Usage
### Remix
To use your dependency within a Remix loader or action, you can access it via
the `services` instance in the
[`FrontCommerceApp`](../04-api-reference/front-commerce-remix/front-commerce-app.mdx)
```mdx-code-block
```
```ts
import { json } from "@front-commerce/remix/node";
import type { LoaderFunctionArgs } from "@remix-run/node";
export const loader = async ({ context }: LoaderFunctionArgs) => {
const app = new FrontCommerceApp(context.frontCommerce);
// highlight-start
const { client } = app.services.DI.acme;
const data = await client.fetchData();
// highlight-end
return json({ data });
};
```
```mdx-code-block
```
```ts
import { json } from "@front-commerce/remix/node";
import type { ActionFunctionArgs } from "@remix-run/node";
export const action = async ({ context }: ActionFunctionArgs) => {
const app = new FrontCommerceApp(context.frontCommerce);
// highlight-start
const { client } = app.services.DI.acme;
const data = await client.fetchData();
// highlight-end
return json({ data });
};
```
```mdx-code-block
```
### GraphQL Module
To use your dependency within a GraphQL module, you can access it via the
`services` in the `context` object.
```mdx-code-block
```
```ts
import { createGraphQLRuntime } from "@front-commerce/core/graphql";
import createFooContextEnhancer from "./createFooContextEnhancer";
export default createGraphQLRuntime({
resolvers: {
Query: {
getAcme: (_, __, { services }) => {
// highlight-start
const { client } = services.DI.acme;
return client.fetchData();
// highlight-end
},
},
},
});
```
```mdx-code-block
```
```ts
import { createGraphQLRuntime } from "@front-commerce/core/graphql";
import createFooContextEnhancer from "./createFooContextEnhancer";
import AcmeLoader from "./AcmeLoader";
export default createGraphQLRuntime({
contextEnhancer: ({ services }) => {
// highlight-start
const { client } = services.DI.acme;
return client.fetchData();
// highlight-end
const Acme = new AcmeLoader(client);
return {
Acme,
};
},
});
```
```mdx-code-block
```
---
# Extend the GraphQL schema
URL: https://developers.front-commerce.com/docs/3.x/guides/extend-the-graphql-schema
{frontMatter.description}
When developing an e-commerce store, you might at some point need to expose new
data in your unified GraphQL schema to support new features and allow frontend
developers to use them. Front-Commerce’s GraphQL modules is the mechanism
allowing to extend and override any part of the schema defined by other modules.
The Front-Commerce core and platform integrations (such as Magento2) are
implemented as GraphQL modules. They leverage features from the GraphQL Schema
Definition Language (SDL).
This page will guide you through the process of exposing a new feature in your
GraphQL schema. We will create an extension with a GraphQL module that allows to
maintain a counter of clicks for a product.
## Create an extension
To extend the GraphQL schema and implement your own logic, you first have to
create an extension. An extension can be created by adding a new folder under
`extensions` and by creating a `index.ts` file (the extension definition) with
the following content:
```typescript title="example-extensions/click-counter/index.ts"
import { defineExtension } from "@front-commerce/core";
export default function clickCounter() {
return defineExtension({
name: "click-counter",
meta: import.meta,
});
}
```
Then you need to
[register the extension into your Front-Commerce project](./register-an-extension.mdx).
## Add a GraphQL module within the extension
For now, the `click-counter` extension does not provide any feature. To extend
the GraphQL schema and implement our own logic, we need to add a GraphQL module
to the extension containing a schema and some resolvers.
### Add an empty GraphQL module
For that, you can create the GraphQL module definition in the file
`example-extensions/click-counter/graphql/index.ts` with the following content:
```typescript title="example-extensions/click-counter/graphql/index.ts"
import { createGraphQLModule } from "@front-commerce/core/graphql";
export default createGraphQLModule({
namespace: "ClickCounter",
});
```
This is a minimal GraphQL module definition which namespace is `ClickCounter`
and that does nothing for now.
Then, the extension needs to declare that it provides a GraphQL module by
modifying the extension's index.ts:
```typescript title="Changes to example-extensions/click-counter/index.ts"
import { defineExtension } from "@front-commerce/core";
import clickCounter from "./example-extensions/click-counter/graphql";
export default function clickCounter() {
return defineExtension({
name: "click-counter",
configuration: {
providers: [],
},
// highlight-start
graphql: {
modules: [clickCounter],
},
// highlight-end
});
}
```
At this point, your Front-Commerce project is configured with an extension
called `click-counter`, this extension provides a GraphQL module which namespace
is `Clickcounter`. The extension is now ready to bring changes to your Graph.
### Extend the GraphQL Schema
Front-Commerce lets you describe your schema using the expressive
[GraphQL Schema Definition Language (SDL)](https://graphql.org/learn/schema/#type-language).
We would like to:
1. add a `clickCounter` field to the existing `Product` type
2. add an `incrementProductCounter` mutation
For that, you can add the type definitions as a `typeDefs` value in your GraphQL
module definition.
```ts title="example-extensions/click-counter/graphql/index.ts"
import { createGraphQLModule } from "@front-commerce/core/graphql";
export default createGraphQLModule({
namespace: "ClickCounter",
// add-start
typeDefs: /** GraphQL */ `
extend type Product {
clickCounter: Int
}
extend type Mutation {
incrementProductCounter(sku: String!, incrementValue: Int): MutationSuccess!
}
`,
// add-end
});
```
:::info
The `Product` type is part of Front-Commerce’s `Front-Commerce/Product` module.
The `MutationSuccess` type, used as the response for the mutation, is part of
Front-Commerce’s core.
:::
At this point, you should be able to see in the GraphQL playground
`http://localhost:4000/graphql` that the type `Product` has a `clickCounter`
field and that a mutation `incrementProductCounter` exists.
You can try to execute the query below in your GraphQL playground
`http://localhost:4000/graphql`:
```graphql title="http://localhost:4000/graphql"
{
product(sku: "WH09") {
sku
clickCounter
}
}
```
Our GraphQL module does not yet have any code for sending content. This means
that even if you can request the `clickCounter` field, it won't actually return
any data. So you should see the following response:
```json
{
"data": {
"product": {
"sku": "WH09",
"clickCounter": null
}
}
}
```
### Define Resolvers and Runtime Logic
The latest step is to implement the logic, which we will refer to as the
`GraphqlRuntime`, to retrieve the `Product.clickCounter` value and behind the
`incrementProductCounter` mutation. For that, you have to add resolvers for the
field and the mutation. This is where most of the _real work_ is done, for
instance by fetching remote data sources or transforming data.
Resolvers are exposed using the `resolvers` key of the GraphQL runtime
definition. It should be a
[**Resolver map**](https://www.graphql-tools.com/docs/resolvers): an object
where each key is a GraphQL type name, and values are mapping between field
names and resolver function. Resolver functions may return a
[Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Using_promises)
for asynchronous operations.
:::note
To learn more about resolvers and their internals, we recommend reading
[GraphQL Tools resolvers documentation](https://www.graphql-tools.com/docs/resolvers).
:::
First, let's update the module definition to register the runtime API:
```typescript title="Changes to example-extensions/click-counter/graphql/index.ts"
import { createGraphQLModule } from "@front-commerce/core/graphql";
export default createGraphQLModule({
namespace: "ClickCounter",
// add-next-line
loadRuntime: () => import("./runtime"), // as this only executes in during runtime, it needs to be an import statement.
});
```
And then let's create the GraphqlRuntime so that we can add the `resolvers` map
as below:
```typescript title="example-extensions/click-counter/graphql/runtime.ts"
import { createGraphQLRuntime } from "@front-commerce/core/graphql";
export default createGraphQLRuntime({
resolvers: {
Product: {
clickCounter: (product) => {
console.log(`click count retrieval for product ${product.sku}`);
// your own retrieval logic here
return 1;
},
},
Mutation: {
incrementProductCounter: (_root, params) => {
const { sku, incrementValue } = params;
console.log(`incrementing counter for product "${sku}"`);
console.log(`counter + ${incrementValue ?? 1}`);
// your own increment logic here
return {
success: true,
};
},
},
},
});
```
It brings a very basic implementation for both the `Product.clickCounter` field
resolution and the `incrementProductCounter` mutation. After restarting
Front-Commerce, the `clickCounter` field of a product should now return `1`
instead of `null`. Likewise, the mutation can also be called from the
playground.
As stated above, a resolver function can also return a Promise and thus handle
asynchronous result. A more real life resolver map would like look like:
```typescript title="example-extensions/click-counter/graphql/runtime.ts"
export default createGraphQLRuntime({
resolvers: {
Product: {
clickCounter: (product) => {
// add-start
const clickCountPromise = fetchClickCountForSku(product.sku);
return clickCountPromise;
// add-end
},
},
Mutation: {
incrementProductCounter: async (_root, params) => {
// add-start
const { sku, incrementValue } = params;
try {
await incrementProductCount(sku, incrementValue);
return {
success: true,
};
} catch (error: any) {
return {
success: false,
errorMessage: error.message,
};
}
// add-end
},
},
},
});
```
## Adding custom `scalars` to the schema
To add
[custom scalars](https://the-guild.dev/graphql/codegen/plugins/typescript/typescript#scalars)
to your schema, you can define them in your GraphQL module as shown below.
```ts
import { createGraphQLModule } from "@front-commerce/core/graphql";
export default createGraphQLModule({
namespace: "example-module",
typeDefs: /** GraphQL */ `
Query {
acmeEntry(identifier: AcmeScalar!): String!
}
`,
// highlight-start
scalars: {
AcmeScalar: "string | number",
},
// highlight-end
});
```
---
# Mutate Data Using Forms
URL: https://developers.front-commerce.com/docs/3.x/guides/mutate-data-using-forms
# Mutate Data Using Forms
In Front-Commerce, you can handle data mutations using Remix's
[``](https://remix.run/docs/en/main/components/form) component combined
with Front-Commerce's GraphQL mutations. This guide explains how to implement
form submissions and handle data mutations effectively.
## Using Remix Forms
Remix uses [``](https://remix.run/docs/en/main/components/form)
component as the primary way to mutate data. When a form is submitted, Remix
will:
1. Call the route's [`action`](https://remix.run/docs/en/main/route/action)
function
2. Process the form data
3. Execute any necessary mutations
4. Return a response (redirect or data)
### Basic Form Structure
Here's a basic example of a password reset form using our theme components:
```tsx title="app/theme/components/PasswordResetForm.tsx"
import { Form } from "@remix-run/react";
import Stack from "theme/components/atoms/Layout/Stack";
import Fieldset from "theme/components/atoms/Forms/Fieldset";
import FormItem from "theme/components/molecules/Form/Item";
import { Password } from "theme/components/atoms/Forms/Input";
import FormActions from "theme/components/molecules/Form/FormActions";
import { SubmitButton } from "theme/components/atoms/Button";
export default function PasswordResetForm() {
return (
);
}
```
## Handling Form Submissions
When the form is submitted, Remix calls the route's
[`action`](https://remix.run/docs/en/main/route/action) function. Here's how to
handle the form submission and perform mutations:
```tsx title="app/routes/reset-password.tsx"
import { redirect, type ActionFunctionArgs } from "@remix-run/node";
import { json } from "@front-commerce/remix/node";
import { FrontCommerceApp } from "@front-commerce/remix";
import { YourMutationDocument } from "~/graphql/graphql";
import PasswordResetForm from "../theme/components/PasswordResetForm";
export async function action({ request, context }: ActionFunctionArgs) {
const app = new FrontCommerceApp(context.frontCommerce);
const formData = await request.formData();
const userInput = Object.fromEntries(formData.entries());
if (userInput.password !== userInput.password_confirm) {
return json({ errorMessage: "Passwords do not match" }, { status: 400 });
}
try {
// Execute GraphQL mutation
const result = await app.graphql.mutate(YourMutationDocument, {
newPassword: userInput.password,
});
if (result.resetPassword?.success) {
return redirect("/success-page");
}
return json({ success: true, message: "Password changed successfully" });
} catch (e) {
return json({ errorMessage: "An error occurred" }, { status: 500 });
}
}
export default function AccountPasswordResetPage() {
return ;
}
```
:::info In the `PasswordResetForm` component above, we used the `action`
property to specify which route will handle the form submission. However, when
using Remix, if your form is defined in the same route that handles its
submission (in this case, `reset-password.tsx`), you can omit the `action`
property. Remix will automatically submit the form to the current route's
`action` function. For more details, see the
[Form API documentation](https://remix.run/docs/en/main/components/form#action).
:::
## Key Points
1. Use Remix's [``](https://remix.run/docs/en/main/components/form)
component
2. Access form data using `request.formData()` in the route's
[`action`](https://remix.run/docs/en/main/route/action) function
3. Use
[`app.graphql.mutate()`](../04-api-reference/front-commerce-remix/front-commerce-app.mdx)
to execute GraphQL mutations
4. Return appropriate responses (redirect, json data or throw an error)
## See also
- [Chocolatine Theme Form Components](../03-extensions/theme-chocolatine/reference/forms.mdx)
- [Form Validation](https://remix.run/docs/en/main/guides/form-validation)
- [Form Resubmissions](https://remix.run/docs/en/main/discussion/resubmissions)
- [Form vs. fetcher](https://remix.run/docs/en/main/discussion/form-vs-fetcher)
---
# Override a component
URL: https://developers.front-commerce.com/docs/3.x/guides/override-a-component
{frontMatter.description}
## Understanding theme overrides
Front-Commerce web application is a fully featured e-commerce
[universal](https://cdb.reacttraining.com/universal-javascript-4761051b7ae9)
React application aimed at providing a sane base for your own theme. Even though
you could reuse pages and components individually in a totally different theme,
most of the time you might find easier and faster to start with the a theme and
iterate from there.
**Theme override is what allows you to:**
- customize existing themes:
- [theme-chocolatine](../03-extensions/theme-chocolatine/_category_.yml)
- upgrade Front-Commerce and benefit from the latest component improvements.
- or create a slightly different theme for events like Black Friday with
confidence!
Having an understanding of
[themes in Magento](https://developer.adobe.com/commerce/frontend-core/guide/templates/),
[child themes in Wordpress](https://developer.wordpress.org/themes/advanced-topics/child-themes/)
/
[Prestashop](https://devdocs.prestashop-project.org/8/themes/reference/template-inheritance/parent-child-feature/),
[templates override in Drupal](https://www.drupal.org/docs/develop/theming-drupal/twig-in-drupal/working-with-twig-templates),
[themes in CakePHP](https://book.cakephp.org/3.0/en/views/themes.html) will help
you understand Front-Commerce’s theme overrides because they all share the same
philosophy. If you have no previous experience with these other implementations,
let’s see how it works!
Your own theme will be located in its own folder and will use default components
from parent theme(s). You would then copy the files you want to override in your
theme folder by maintaining an identical file structure. Your component will
then be used instead of the one in your parent theme(s).
This translates in those three steps:
1. configure your custom theme and use it in your application
2. copy the file (`jsx`, `scss` or `gql`) you want to override in the `theme`
folder of the `app` directory
3. customize its content in your extension directory
## Creating the `theme` folder
:::info
Whatever method you use, we will refer to this folder as "theme" for the rest of
this documentation
:::
### Using the skeleton as base
First we need to create the `theme` directory in your `app` folder
```
my-project
└── app
└── theme
```
That’s it! You have successfully created the folder that will be the root of
your custom theme!
### In your own extension
You can also extend theme in your extension, please read
[Register an extension](./register-an-extension.mdx) guide to know how to create
your own extension.
First in your extension definition file, add the `theme` entry:
```javascript title="extension/theme-acme/index.ts"
import path from "path";
import { defineExtension } from "@front-commerce/core";
export default function themeAcme() {
return defineExtension({
name: "AcmeTheme",
// highlight-next-line
theme: "extensions/theme-acme/theme",
configuration: {
providers: [],
},
});
}
```
And now theme file will be injected in your project!
## Override a component
Let's add the description of a `Product` to a `ProductItem` as an example of
overriding the base theme.
The original file is:
[`node_modules/@front-commerce/theme-chocolatine/theme/modules/ProductView/ProductItem/ProductItem.jsx`](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/blob/main/packages/theme-chocolatine/theme/modules/ProductView/ProductItem/ProductItem.jsx)
:::info
Please refer to the
[Front-Commerce’s folder structure documentation page](../05-concepts/react-components-structure.mdx)
to get a better understanding of how components are organized in the base theme.
:::
1. copy it to: `app/theme/modules/ProductView/ProductItem/ProductItem.jsx`
2. add the description after the `` component in your
`ProductItem` with `{props.description}`.
**But you are not done yet!** The `description` information is not included in
the GraphQL fields fetched by the application in the base theme. You will thus
need to update the fragment related to `ProductItem`.
:::info
In this case, the data fetching is made using a GraphQL request. Since our
component use data fetched by a GraphQL request, we need to update this fragment
to add the data we want to the request.
We invite you to read the
[request data flow](../05-concepts/a-request-data-flow.mdx) to learn more about
how data are fetched in Front-Commerce.
:::
The original fragment file is collocated with the original component at:
1. copy the
[`original ProductItemFragment`](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/blob/main/packages/theme-chocolatine/theme/modules/ProductView/ProductItem/ProductItemFragment.gql?ref_type=heads)
to the equivalent location in your extension.
2. add the field `description` to the fragment.
```diff title="app/theme/modules/ProductView/ProductItem/ProductItemFragment.gql"
fragment ProductItemFragment on Product {
imageUrl
+ description
...ProductOverviewFragment
...ProductItemActionsFragment
}
```
3. restart the application
The data will now be fetched from GraphQL every time the `ProductItem` is used
(product listings, upsells…) and will be available for you to render it as
wanted.
### Reuse the original component
Front-Commerce has been designed with small components having a single
responsibility. **We believe that theme overrides should cover most of your use
cases**. For some features though, it appeared that reusing the base component
could be really useful. For instance, if you want to add a small feature or
wrapper without changing the core feature of a component.
It is possible to import a component from the `@front-commerce/theme/` module
instead of `theme`. It is similar to if you were importing components from other
libraries.
:::note
**Use with care!** We don't think that this method should be the default one,
because it can make your updates more painful.
:::
In the file that we've created in the previous section, instead of copying the
original source file you could set it up like this:
```jsx title="theme/modules/ProductView/ProductItem/ProductItem.jsx"
import BaseProductItem from "@front-commerce/theme/modules/ProductView/ProductItem/ProductItem.js";
const ProductItem = (props) => (
All extensions of a Front-Commerce application are registered through the
`front-commerce.config.ts` configuration file at the root of the project by
importing the extension definitions (or functions returning an extension
definition) and adding them the `extensions` list.
For instance, in a project configured to be connected to a Magento2 instance and
with the theme chocolatine,
[the Magento2 extension](./register-an-extension.mdx) and
[the Theme Chocolatine extension](../03-extensions/theme-chocolatine/_category_.yml)
are registered in `front-commerce.config.ts`:
```typescript title="front-commerce.config.ts"
import { defineConfig } from "@front-commerce/core/config";
import themeChocolatine from "@front-commerce/theme-chocolatine";
import magento2 from "@front-commerce/magento2";
import storesConfig from "./app/config/stores";
import cacheConfig from "./app/config/caching";
export default defineConfig({
// highlight-next-line
extensions: [magento2({ storesConfig }), themeChocolatine()],
stores: storesConfig,
cache: cacheConfig,
configuration: {
providers: [],
},
});
```
In that project, if you want to add an extension, you could add the following
changes to `front-commerce.config.ts`:
```typescript title="front-commerce.config.ts"
import { defineConfig } from "@front-commerce/core/config";
import themeChocolatine from "@front-commerce/theme-chocolatine";
import magento2 from "@front-commerce/magento2";
// highlight-next-line
import aCustomExtension from "./extensions/custom-extension";
import storesConfig from "./app/config/stores";
import cacheConfig from "./app/config/caching";
export default defineConfig({
// highlight-next-line
extensions: [
magento2({ storesConfig }),
themeChocolatine(),
aCustomExtension(),
],
stores: storesConfig,
cache: cacheConfig,
configuration: {
providers: [],
},
});
```
where `./extensions/custom-extension/index.ts` would be something like:
```typescript title="extensions/custom-extension/index.ts"
import { defineExtension } from "@front-commerce/core";
export default function aCustomExtension() {
return defineExtension({
name: "custom-extension",
configuration: {
providers: [],
},
});
}
```
---
# Translate your application
URL: https://developers.front-commerce.com/docs/3.x/guides/translate-your-application
{frontMatter.description}
import Tabs from "@theme/Tabs";
import TabItem from "@theme/TabItem";
:::note
If you want to configure your application to support multiple languages, please
refer to
[Configure multiple stores](./configuration/04-configure-multiple-stores.mdx).
:::
Front-Commerce manages your translations by using
[react-intl](https://formatjs.io/docs/getting-started/application-workflow), a
standard library in the React ecosystem. This library works by transforming your
static values in React Components. These components will then fetch your
translations and display them in your application.
## Declare translations in your application
:::info
You can find the full set of components in the
[React Intl Components documentation](https://formatjs.io/docs/react-intl/components/).
:::
For instance, let's see how to transform your values in `react-intl` Components.
### Strings
```tsx
import { FormattedMessage } from "react-intl";
;
```
:::note
To take full advantage of the `translate` cli in Front-Commerce, a default
message is required for formatted messages. This will be used to extract the
messages for the default locale and ensure there are no dead translations, or
missing translations.
:::
### Dates
```tsx
import { FormattedDate } from "react-intl";
;
```
### Prices
```tsx
import { FormattedNumber } from "react-intl";
;
```
:::note
However, for this particular use case we have abstracted this in Front-Commerce
with the
[theme/components/atoms/Price](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/blob/c8fc25465ab7228098bf45c3cbc712c8d2c32529/packages/theme-chocolatine/theme/components/atoms/Typography/Price/index.js)
component (and its variants: `ProductPrice`, `PriceOrFree`). We recommend you to
use it instead, so you could benefit from better integration with its related
GraphQL Type.
:::
### Without React Component
These components will be enough in most cases. However, the issue with these is
that it wraps your string within a React Element, which may be troublesome when
you actually need to handle the string itself.
For instance, this is the case when you want to add some label attributes to
your DOM elements.
```jsx
```
Fortunately, this is correctly handled by `react-intl` if you use
[`defineMessages`](https://formatjs.io/docs/react-intl/api/#definemessagesdefinemessage)
combined with the
[`useIntl`](https://formatjs.io/docs/react-intl/api/#useintl-hook) hook or the
[`injectIntl`](https://formatjs.io/docs/react-intl/upgrade-guide-3x/#new-useintl-hook-as-an-alternative-of-injectintl-hoc)
HOC.
```jsx
import { defineMessages, useIntl } from "react-intl";
const messages = defineMessages({
ariaLabel: {
id: "screen-reader-icon-title",
defaultMessage: "Icon title displayed for screen readers",
},
});
const MyComponentWithHook = (props) => {
const intl = useIntl();
return (
);
};
```
:::caution IMPORTANT
You should always define a `defaultMessage`, and preferably in your default
locale which you will use with the `translate` command. This will allow
`FormatJS` to extract the existing messages for your application.
:::
## Translate what's in your components
Now that you have defined what should be translated in your application, you
actually need to translate your application. This is a two-step process:
1. Run the following script that will fetch all your translatable strings in
your application and gather them in a JSON file located in
`lang/[locale].json`
```shell
$ front-commerce translate --locale
```
:::tip ProTip™
The script has already been defined in the
[skeleton template](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/blob/c8fc25465ab7228098bf45c3cbc712c8d2c32529/skeleton/package.json#L16),
and can be used directly with `pnpm run translate`. You can change the
default locale to accommodate your store.
:::
2. Translate the strings available in those files, and you are good to go 🙂
If your default locale is `en` and you want to add support for `fr` you can
create a `fr.json` in your `lang` folder and translate the strings for each
key.
:::tip ProTip™
You can use tools like [BabelEdit](https://www.codeandweb.com/babeledit) to
help translate and keep your translations in sync.
:::
### Translations fallback
With `react-intl` translations are usually grouped into a single file. In our
case, we would expect them to be in `lang/[locale].json`. but we don't want you
to be troubled by translations that are handled by the core.
That's why Front-Commerce uses a mechanism called **translations fallbacks**.
Instead of relying on a single file for translations, Front-Commerce will look
out for translations that are defined by extensions
#### From your extension [declared in `front-commerce.config.js`](./register-an-extension.mdx)
If you are developing an extension, you will need to add the path to your
translations in the extension configuration file, for example lets say your
translations are located in `/lang/[locale].json`.
You can then run the following command command to extract the translations from
your extension:
```bash
$ pnpm run front-commerce translate /**/*.{js,jsx,ts,tsx} --locale
```
And then in your extension definition file you should add the `translations`
option:
```ts title="extensions/acme-extension/index.ts"
import { defineExtension } from "@front-commerce/core";
export default defineExtension({
// ...
translations: `extensions/acme-extension/lang`, // path to your translations
});
```
This will extract the translations in your extension to the following file:
- `/lang/.json`
It will also allow it to be injected into the generated translation file.
#### From your project
- `lang/[short-locale].json` or
- `lang/[locale].json`
`[short-locale]` here means that we are only taking the first particle of the
`locale`. E.g. if the locale was `en-GB`, the short-locale would be `en`. That's
how `en-GB` would load translations from both `en.json` and `en-GB.json` files.
If a translation key is defined in multiple files, the last one (according to
the above list) will be be used. This is especially useful if you want to change
the core's translations.
:::tip ProTip™
You can see exactly which translation files are used by opening the files
located in .front-commerce/compiled-lang/.
:::
With `react-intl` translations are usually grouped into a single file. In our
case, we would expect them to be in `lang/[locale].json`, we don't want you to
be troubled by translations that are handled by the core.
Please keep in mind that when you run `pnpm run translate`, new keys will be
added to `lang/[locale].json` as the script only detects locales in the `app`
folder.
## About dynamic content
`react-intl` lets you translate your static content. But what about dynamic
content? Things like product title and description, CMS pages, etc.
This is done on the GraphQL side. The principle is that when a user is connected
to Front-Commerce, they will be assigned a `storeViewCode` in their session (see
[Configure multiple stores](./configuration/04-configure-multiple-stores.mdx)
for more details).
This code will then be used by your GraphQL loaders to retrieve the correct
content from your backend.
## Loaders and Meta function
The
[`FrontCommerceApp`](../04-api-reference/front-commerce-remix/front-commerce-app.mdx)
instance exposes the [`intl` object](https://formatjs.io/docs/intl) which can be
used to translate strings in your loaders, which can then be used in in the meta
function.
```tsx title="app/routes/my-route.ts"
import { json } from "@front-commerce/remix/node";
import type { LoaderFunctionArgs, MetaFunction } from "@remix-run/node";
import { FrontCommerceApp } from "@front-commerce/remix";
import { defineMessage } from "react-intl";
const documentTitle = defineMessage({
id: "pages.my-route.document-title",
defaultMessage: "My Route",
});
export const loader = async ({ context }: LoaderFunctionArgs) => {
const app = new FrontCommerceApp(context.frontCommerce);
return json({
// highlight-next-line
title: app.intl.formatMessage(messages.documentTitle),
});
};
export const meta: MetaFunction = (args) => {
// This will contain the translated title returned from the loader function.
// highlight-next-line
return [{ title: args.data.title }];
};
```
---
# Debug flags
URL: https://developers.front-commerce.com/docs/3.x/api-reference/debug-flags/index
{frontMatter.description}
import Heading from "@theme/Heading"; // Import Heading component
import flags from "./debug-flags.json";
## How to use
Debug flags enable specific logs in Front-Commerce, helping developers to debug
issues or understanding application behavior. To enable a debug flag, set the
`DEBUG` environment variable in your project's root `.env` file. The `DEBUG`
variable should be a comma-separated list of debug flags you wish to enable.
Wildcards can be used to enable multiple flags at once.
Example:
```sh
# .env
DEBUG="front-commerce:graphql,front-commerce:adobe-b2b*
```
You can also use debug flags in CLI.
Example:
```sh
# bash
$ DEBUG="front-commerce:axios" pnpm dev
```
## Convention
Debug flags follow this naming convention:
```
front-commerce:::
```
For cross-package features:
```
front-commerce:
```
```mdx-code-block
import { Fragment } from "react"
export const JSONList = () => {
return (
<>
axios
))}
>
);
};
```
---
# Content Composition
URL: https://developers.front-commerce.com/docs/3.x/api-reference/front-commerce-core/content-composition
{frontMatter.description}
## References
- [Content Composition Guide](../../02-guides/content-composition/index.mdx)
- [React Hooks](./react.mdx#usecompositioncomponentmap)
## API
### `register` lifecycle hook
A method that allows you to register a new composition or extend an existing
one.
#### Parameters
- `name: string` - The name of the composition
- `collection:`[`CompositionCollection`](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/blob/88efc84b482207f5ab215955dfd9a294b2d8da9b/packages/core/extensions/content-composition/ContentComposition.ts#L12) -
An array of composition entries.
#### Example
```ts title="./example-extension/acme-extension/index.ts"
export default defineExtension({
unstable_lifecycleHooks: {
onContentCompositionInit: (composition) => {
composition.register("Wysiwyg", [
{
// Since this exists, it will override the existing composition
name: "DefaultWysiwyg",
client: {
component: new URL(
"./components/DefaultWysiwyg.tsx",
import.meta.url
),
},
fragment: /* GraphQL */ `
fragment DefaultWysiwygFragment on DefaultWysiwyg {
childNodes
}
`,
},
{
// This will be added to the composition for `Wysiwyg`
name: "CustomWysiwyg",
client: {
component: new URL(
"./components/MagentoWysiwyg.tsx",
import.meta.url
),
},
fragment: /* GraphQL */ `
fragment CustomWysiwygFragment on CustomWysiwyg {
childNodes
}
`,
},
]);
},
},
});
```
### `createContentComposition` helper
This method is a helper to create a `Content Composition` collection, which can
then be used in the `onContentCompositionInit` lifecycle hook to register your
composition.
#### Parameters
See [`register`](#register-lifecycle-hook) for more information.
#### Example
```ts title="./example-extension/acme-extension/index.ts"
import { createContentComposition } from "@front-commerce/core";
const WysiwygComposition = createContentComposition("Wysiwyg", [
{
// Since this exists, it will override the existing composition
name: "DefaultWysiwyg",
client: {
component: new URL("./components/DefaultWysiwyg.tsx", import.meta.url),
},
fragment: /* GraphQL */ `
fragment DefaultWysiwygFragment on DefaultWysiwyg {
childNodes
}
`,
},
{
// This will be added to the composition for `Wysiwyg`
name: "CustomWysiwyg",
client: {
component: new URL("./components/MagentoWysiwyg.tsx", import.meta.url),
},
fragment: /* GraphQL */ `
fragment CustomWysiwygFragment on CustomWysiwyg {
childNodes
}
`,
},
]);
export default defineExtension({
unstable_lifecycleHooks: {
onContentCompositionInit: (composition) => {
composition.registerComposition(WysiwygComposition);
},
},
});
```
---
# Release notes
URL: https://developers.front-commerce.com/docs/3.x/upgrade/release-notes
{frontMatter.description}
## Latest version
**Front-Commerce [`3.12.0`](#3120-2025-03-27)**
Compatible with:
- **Node.js:**: ^20.12.0
- **Gezy**: v11
- **Magento2**: 2.4.4 -> 2.4.6-p3 (Open Source & Commerce & B2B)
- **Magento1**: CE 1.7+, EE 1.12+,
[OpenMage LTS](https://www.openmage.org/supported-versions.html) 19.4+
## Semantic Versioning
We aim at releasing often and follow [Semantic Versioning](https://semver.org)
to clearly communicate our advancements to developers. We document each
migration process (changelog, release notes and documentation updates) and add
deprecation warnings while keeping backwards compatibility to keep upgrades as
seamless as possible.
**TL;DR:** (from the [Semantic Versioning](https://semver.org) documentation)
> Given a version number MAJOR.MINOR.PATCH, increment the:
>
> 1. MAJOR version when you make incompatible API changes,
> 2. MINOR version when you add functionality in a backwards compatible manner,
> and
> 3. PATCH version when you make backwards compatible bug fixes.
>
> Additional labels for pre-release can (and in our case will) be added to the
> MAJOR.MINOR.PATCH format.
Each supported version will receive bug fixes and security updates, leading to
regular patch releases.
## Release Schedule
We release a minor version of Front-Commerce **every 6 weeks** with new
features, tech improvements, bug fixes…
In order to follow the pace of our users, we may release intermediate
pre-releases (`-beta.x`) between 2 minor releases, to allow early-adopters to
integrate the features as soon as possible and to give us feedback.
The table below shows the exact dates of the upcoming releases and our Long Term
Support (LTS) policy.
There are three phases that a Front-Commerce release can be in: Current, End of Support (EOS), and End of Life (EOL)
- `Current` - Should incorporate most of the non-major (non-breaking) changes
that land on Front-Commerce `main` branch.
- `End of Support` - While still in the `EOS` phase, the release will receive
critical bug fixes and security updates. New features will not be added to
`EOS` releases. The `EOS` phase will last for 1 year after the release date.
- `End of Life` - While still in the `EOL` phase, the release will receive
critical security updates. No new features or bug fixes will be added to `EOL`
releases. The `EOL` phase will last for 2 year after the release date.
## 3.12.0 (2025-03-27)
This release brings complete 3.x documentation with LLM optimization, new Gezy
features including RMA workflows, sitemap generation, and product customization
capabilities, along with improved testing infrastructure.
- [Announcement](/changelog/front-commerce-3.12)
- [Migration guide](/docs/3.x/upgrade/migration-guides/3.11-3.12)
- [Changelog](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/releases/3.12.0)
## 3.11.0 (2025-02-13)
This release introduces the **Content Composition API**, the **Prismic
extension** and Magento 1 customer and cart cache.
- [Announcement](/changelog/front-commerce-3.11-2.x-LTS)
- [Migration guide](/docs/3.x/upgrade/migration-guides/3.10-3.11)
- [Changelog](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/releases/3.11.0)
## 3.10.0 (2025-01-09)
This release introduces the HiPay extension for 3.x, Gezy improvements and
diverse smaller features.
- [Announcement](/changelog/front-commerce-3.10-2.35)
- [Migration guide](/docs/3.x/upgrade/migration-guides/3.9-3.10)
- [Changelog](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/releases/3.10.0)
## 3.9.0 (2024-11-21)
This release introduces Google Consent Mode V2, Consistent Price Display, Gezy
advanced capabilities and Extension Features API.
- [Announcement](/changelog/front-commerce-3.9-2.34)
- [Migration guide](/docs/3.x/upgrade/migration-guides/3.8-3.9)
- [Changelog](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/releases/3.9.0)
## 3.8.0 (2024-10-10)
This release marks a major milestone for our Gezy integration, introducing core
e-commerce functionalities.
- [Announcement](/changelog/front-commerce-3.8-2.33)
- [Migration guide](/docs/3.x/upgrade/migration-guides/3.7-3.8)
- [Changelog](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/releases/3.8.0)
## 3.7.0 (2024-08-29)
This release introduces te foundations for the Gezy integration and several bug
fixes to improve the overall stability of the platform.
- [Announcement](/changelog/front-commerce-3.7-2.32)
- [Migration guide](/docs/3.x/upgrade/migration-guides/3.6-3.7)
- [Changelog](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/releases/3.7.0)
## 3.6.0 (2024-07-18)
This release introduces a revamped Maintenance Mode, convenients API to define
`Cache-Control` and `Server-Timing` headers to improve performance and
monitoring. It also includes several bug fixes to improve developer experience
and the overall stability of the platform.
- [Announcement](/changelog/front-commerce-3.6-2.31)
- [Migration guide](/docs/3.x/upgrade/migration-guides/3.5-3.6)
- [Changelog](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/releases/3.6.0)
## 3.5.0 (2024-06-06)
This release add the quick order page which and different ways to switch to
server-side tracking to cope with the decline of 3rd-party cookies. It
also contains dynamic routing, built on top of the Remix/React-Router routes
API.
- [Announcement](/changelog/front-commerce-3.5-2.30)
- [Migration guide](/docs/3.x/upgrade/migration-guides/3.4-3.5)
- [Changelog](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/releases/3.5.0)
## 3.4.0 (2024-04-25)
We migrated to Vite, a faster development tool providing a significantly
improved development experience with instant feedback. We introduced new
features like stabilized public core APIs, new payment modules, and enhanced
testing.
- [Announcement](/changelog/front-commerce-3.4-2.29)
- [Migration guide](/docs/3.x/upgrade/migration-guides/3.3-3.4)
- [Changelog](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/releases/3.4.0)
## 3.3.0 (2024-02-01)
The release introduces event-driven architectural components, extension
features, public configurations, and automatic session commit. It also includes
core enhancements and smaller convenient features. It sets the technical
foundations for future developments and enables the development of event-based
features in applications.
- [Announcement](/changelog/front-commerce-3.3/)
- [Migration guide](/docs/3.x/upgrade/migration-guides/3.2-3.3)
- [Changelog](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/releases/3.3.0)
## 3.2.0 (2024-01-08)
This updates includes some bug fixes and improvements to the core. Notably, new
packages such as `@front-commerce/magento1`, `@front-commerce/adyen` and
`@front-commerce/akamai-image-manager` are now available. The Magic Button and
Contribution technical foundations have been revamped to embrace Web standards.
- [Announcement](/changelog/front-commerce-3.2-2.28/)
- [Migration guide](/docs/3.x/upgrade/migration-guides/3.1-3.2)
- [Changelog](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/releases/3.2.0)
## 3.1.0 (2023-11-20)
> This update includes several core stabilization fixes, a new PWA assets
> generation API, `useCart` and `useCustomer` hooks and Magento2 order
> filtering. It also features Attraqt Datasource Compatibility for Magento1,
> extended support for product reviews to Magento1, and facets order
> customization for Magento1. Additionally, the update improves the performance
> of adding multiple items to the cart in Magento2, particularly beneficial for
> Adobe Commerce B2B projects
- [Announcement](/changelog/front-commerce-3.1-2.27/)
- [Migration guide](/docs/3.x/upgrade/migration-guides/3.0-3.1)
- [Changelog](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/releases/3.1.0)
## 3.0.0 (2023-09-28)
We are excited to introduce Front-Commerce 3.0, a big leap forward in e-commerce
development. With a focus on web performance, developer experience, and
adherence to web standards, this release is set to redefine how you approach
building your online storefronts.
Front-Commerce is a storefront for headless commerce based on the newly released
Remix v2 framework. We're bringing all the goodness we developed over the past 7
years, to the Remix ecosystem.
In this release, we've merged both technologies to enhance the experience for
end users and developers alike.
- [Announcement](/changelog/front-commerce-3.0/)
- [Changelog](https://gitlab.blackswift.cloud/front-commerce/front-commerce/-/releases/3.0.0)