# 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 (

Hello {name}!

Contact us:

); } export function ErrorBoundary() { const error = useRouteError(); return (
Oops, something bad happened !
{error.toString()}
); } ``` # 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.
![Storybook interface with the IllustratedContent's default story selected](./assets/illustrated-content-story.png)
:::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 ```
![Storybook interface with many IllustratedContent's stories](./assets/many-illustrated-content-stories.png)
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:
![Storybook interface with the IllustratedContent's default story selected](./assets/illustrated-content-stories-docs.png)\*
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 `` component](./display-a-map) that would be a better candidate for a real project. In order to learn Front-Commerce we prefer to document how to do things yourself. As a stretch goal, you can try to replace your components with the `` one. ::: ### 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
Loading...
; } if ((!loading && !store) || error) { return
Oops, an error occurred.
; } const coordinates = [store.coordinates.longitude, store.coordinates.latitude]; const defaultZoom = 14; 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 (
Change password
); } ``` ## 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) => (
{/* Add your feature here */}
); export default ProductItem; ``` --- # Register an extension URL: https://developers.front-commerce.com/docs/3.x/guides/register-an-extension

{frontMatter.description}

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
  • Debugs Axios HTTP requests and responses
{Object.entries(flags).map(([name, descriptions]) => ( {name}
    {descriptions.map((description) => (
  • {description}
  • ))}
))} ); }; ``` --- # 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)