Skip to main content
Version: 2.x

Create a Business Component

In this section, we will build a Business Component. The core concept is the same as creating a UI component.

In Front-Commerce we have separated our components in two categories: the UI components available in the web/theme/components folder, and the Business components available in the web/theme/modules and web/theme/pages folders.

note

If you would like to understand why we went for this organization, feel free to refer to React components structure first.

What is a Business component

A Business component will be located in the my-module/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 from your Pages components.

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 folder.

note

They are often a mix between UI components and custom layout. They may be split in multiples 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 ones are often smart and contain logic. We try to extract this logic in Enhancers, 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 package. It provides a component that uses leaflet under the hood. It will allow us to display the position of our store within OpenStreetMap.

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):

npm install leaflet@^1.7 react-leaflet@3.2.5
note

Front-Commerce also provides a <Map> component 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 <Map> 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. Once you have one, the goal will be to override the Home component from node_modules/front-commerce/src/web/theme/pages/Home/Home.js to my-module/web/theme/pages/Home/Home.js.

danger

Do not forget to restart your application (npm run start) in case the override do not work. There is an upcoming improvement that should make things easier in the future (see #63).

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:

my-module/theme/pages/Home/Home.js
// ... 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 = ({ store }) => (
<div className="page page--home">
{/* ... The rest of the homepage */}
<MapContainer
center={[43.584296, 1.44182]}
zoom={14}
style={{ height: "600px", width: "800px" }}
>
<TileLayer
attribution='&amp;copy <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
<Marker position={[43.584296, 1.44182]}>
<Popup>
<span>My awesome store is HERE!</span>
</Popup>
</Marker>
</MapContainer>
</div>
);
// ...

With that, you should see the map appear in your homepage.

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 (i.e: define imgSrc: ["*.openstreetmap.org"]).

Extracting our new component

Having the map in the Home component could be fine for a time, but if there are many other features in the Home, it becomes hard to maintain. So when the Home becomes big enough, we extract the Store Locator into its own module component.

To do so, we will reuse the exact same component but move it into its own module in my-module/theme/modules.

my-module/theme/modules/StoreLocator/StoreLocator.js
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 = (props) => {
return (
<MapContainer
center={[43.584296, 1.44182]}
zoom={14}
style={{ height: "600px", width: "800px" }}
>
<TileLayer
attribution='&amp;copy <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
<Marker position={[43.584296, 1.44182]}>
<Popup>
<span>My awesome store is HERE!</span>
</Popup>
</Marker>
</MapContainer>
);
};

StoreLocator.propTypes = {};

export default StoreLocator;

In order to make it consistent with other components in the application, we will add two more files:

  • my-module/web/theme/modules/StoreLocator/index.js: will proxy the StoreLocator.js file in order to be able to do imports on the folder directly. See this blog post for more context about this pattern.
my-module/web/theme/modules/StoreLocator/index.js
import StoreLocator from "./StoreLocator";

export default StoreLocator;
  • my-module/web/theme/modules/StoreLocator/StoreLocator.story.js: 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.
my-module/web/theme/modules/StoreLocator/StoreLocator.story.js
import StoreLocator from "./StoreLocator.js";
import { storiesOf } from "@storybook/react";

storiesOf("modules.StoreLocator", module).add("default", () => {
return <StoreLocator />;
});
note

We won't focus on the story in this guide. But you can refer to the Storybook guide to learn how to add any kind of stories to your Storybook.

Fetching our data

Hardcoded values are perfectly fine. But if the coordinates change overtime, it might be a good idea to fetch them dynamically. This is what we will do in this example.

Here, we have to introduce a new concept we use in Front-Commerce: Enhancers. In this specific case, the Enhancer will be responsible for fetching the data from our GraphQL schema, transform the data and feed it to the StoreLocator component.

But the Enhancers are not meant to only be data fetchers, they contain most of the Business logic of our application, we use the Higher-Order Components pattern (HOC) to create them.

note

In Front-Commerce, we use a react library: Recompose to handle composition of HOC, it provides a lot of helpers which are really useful to enrich our components.

Thus, to fetch our data from GraphQL, we are going to create an Enhancer for our store locator. it will be responsible of fetching and transforming the store information to match our needs.

my-module/theme/modules/StoreLocator/EnhanceStoreLocator.js
import { graphql } from "react-apollo";

export default ({ StoreLocatorQuery }) =>
graphql(StoreLocatorQuery, {
props: ({ data }) => ({
loading: data.loading,
error: data.error,
store: data.loading ? null : data.store,
}),
});

Here, we are using the graphql function which allows us to fetch data in our graphQL schema.

You can find all the available graphql options in the React Apollo documentation

In our case, it takes two arguments:

  • the first one is the Query we need to fetch (we will handle this part soon)
  • the props property in the second parameter to compute the Query result data to loading, error and store properties that will be used in the enhanced component.

As you can see, our Enhancer needs a Query (StoreLocatorQuery). This is a .gql file that use the GraphQL syntax. In our case, it will look like:

my-module/theme/modules/StoreLocator/StoreLocatorQuery.gql
query StoreLocator {
store {
name
phone
owner {
email
}
coordinates {
longitude
latitude
}
}
}

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.

note

You may think that some queries are already launched in our EnhanceHome and that splitting the StoreLocatorQuery from them is inefficient. But react-apollo will handle that for you. It will batch the requests to avoid too many network roundtrips. This allows us to only think about what a component needs. The responsibility for retrieving its data lies with it and allows us to use it anywhere.

However, if it is important in your case to fuse your queries, you may be interested by the concept of Fragments in GraphQL. It allows you to split part of your queries without splitting the end query.

Making it dynamic

Now that we have our Enhancer ready, we are going to use it in our store locator. The significant change here is that your data comes from your Enhancer and is passed down to your component by props.

But 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.

my-module/theme/modules/StoreLocator/StoreLocator.js
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";
import StoreLocatorQuery from "./StoreLocatorQuery.gql";
import EnhanceStoreLocator from "./EnhanceStoreLocator";

const StoreLocator = (props) => {
if (props.loading) {
return <div>Loading...</div>;
}
if (props.error) {
return <div>Oops, an error occurred.</div>;
}

const coordinates = [
props.store.coordinates.longitude,
props.store.coordinates.latitude,
];
const defaultZoom = 14;

return (
<div>
<MapContainer
center={coordinates}
zoom={defaultZoom}
style={{ height: "600px", width: "800px" }}
>
<TileLayer
attribution='&amp;copy <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
<Marker position={coordinates}>
<Popup>
<div>
My awesome store ─ {props.store.name}
Email: {props.store.owner.email}
Phone: {props.store.phone}
</div>
</Popup>
</Marker>
</MapContainer>
</div>
);
};

StoreLocator.propTypes = {
loading: PropTypes.bool.isRequired,
store: PropTypes.shape({
name: PropTypes.string.isRequired,
phone: PropTypes.string.isRequired,
owner: PropTypes.shape({
email: PropTypes.string.isRequired,
}).isRequired,
coordinates: PropTypes.shape({
longitude: PropTypes.number.isRequired,
latitude: PropTypes.number.isRequired,
}).isRequired,
}),
};

// Let's not forget to use the Enhancer
export default EnhanceStoreLocator({ StoreLocatorQuery })(StoreLocator);
About PropTypes

They allow us to validate the props passed to a child component, this will log explicit errors if you pass invalid props in dev mode. This will also serve as documentation for other developers in your team. They may be a little verbose, but maintaining them will help you in the long run.

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.

my-module/theme/pages/Home/Home.js
import StoreLocator from "theme/modules/StoreLocator";

const Home = ({ store }) => (
<div className="page page--home">
{/* ... */}
<StoreLocator />
</div>
);

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 my-module/web/theme/pages/Home/Home.js, we do not have to import the StoreLocator by using relative paths ../../modules/StoreLocator but we can remain with theme/modules/StoreLocator which is more explicit. This is possible for any file located in the folder web/theme of a module.

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.).

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.