Create a Business Component In this guide, we learn how to 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
app/theme/components
folder, and the Business components available in the
app/theme/modules
and app/theme/pages
folders.
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 .
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.
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 .
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:
Displaying a map on the homepage
Fetching the position of the store from the backend
Link both to have an actual module
1 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):
pnpm install leaflet@^1.7 react-leaflet@3.2.5
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.
2 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/theme-chocolatine/theme/pages/Home/Home.js
to app/theme/pages/Home/Home.js
.
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:
app/theme/pages/Home/Home.tsx
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 = ( ) => ( < div className = " page page--home " > { } < MapContainer center = { [ 43.584296 , 1.44182 ] } zoom = { 14 } style = { { height : "600px" , width : "800px" } } > < TileLayer attribution = ' & 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.
3 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
.
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 ( < MapContainer center = { [ 43.584296 , 1.44182 ] } zoom = { 14 } style = { { height : "600px" , width : "800px" } } > < TileLayer attribution = ' & 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 > ) ; } ; 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
for more context about this pattern.
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.
app/theme/modules/StoreLocator/StoreLocator.stories.tsx
import StoreLocator from "./StoreLocator" ; import type { Meta , StoryObj } from "@storybook/react" ; const meta : Meta < typeof StoreLocator > = { component : StoreLocator , } ; export default meta ; type Story = StoryObj < typeof StoreLocator > ; export const Default : Story = { args : { } , } ;
We won't focus on the story in this guide. But you can refer to the
Storybook guide to learn how to
4 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 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 < typeof loader > ( "/api/store-locator" ) ; const { data , state } = fetcher . load ( ) ; return { loading : state === "loading" , error : data ?. error , store : data ?. store , } ; }
Here, we are using the
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.
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:
app/theme/modules/StoreLocator/StoreLocatorQuery.gql
query StoreLocator { store { name phone owner { email } coordinates { longitude latitude } } }
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.
5 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.
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 < div > Loading... </ div > ; } if ( ( ! loading && ! store ) || error ) { return < div > Oops, an error occurred. </ div > ; } const coordinates = [ store . coordinates . longitude , store . coordinates . latitude ] ; const defaultZoom = 14 ; return ( < div > < MapContainer center = { coordinates } zoom = { defaultZoom } style = { { height : "600px" , width : "800px" } } > < TileLayer attribution = ' & 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 > ) ; } ; 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.
app/theme/pages/Home/Home.tsx
import StoreLocator from "theme/modules/StoreLocator" ; const Home = ( ) => ( < 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 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.).
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.