Broadleaf Microservices
  • v1.0.0-latest-prod

Component Organization

There are several ways in which we have categorized the files in the folder structure. It is important when placing these files that we think about what area of the application the files support as well as their functional behavior.

Tip
Since we’re using TypeScript, instead of .js (plain JS) and .jsx (component) files, we have .ts and .tsx. .tsx means that the file uses JSX which is a syntax for creating components that looks like HTML.

ES6 Modules

React is built around components. There can be simple components like Button that is simply a component version of HTML <button>. There can be compound components like a Table that is made up of multiple smaller components: Row, Headers, Cell, Caption. Moreover, every component can have a related test file, utils, and hooks. Thus, we need a way group all of these related files together.

This grouping we’ll call a "module" (a la ES6 modules). Modules are determined by whether they represent a page, their domain, and their function.

Pages and Routing

Files representing pages on the site go into pages/ where the directory structure matches the URL that should map to that page. Thus, the homepage (/) maps to pages/index.tsx, the My Account dashboard (/my-account) maps to /my-account/index.tsx, and so on.

For dynamic URLs like for PDPs, path variables can be denoted using [] in the file name. /details/[…​url].tsx would then map any URL that starts with /details to that component, e.g., /details/green-ghost. Path variables can then be accessed in getServerSideProps via the params prop:

// if url is the variable
export async function getServerSideProps({
  req,
  res,
  params,
}: GetServerSidePropsContext): Promise<ServerSideProps> {
  const { url } = params;
  return { props: { url } };
}
Important

Use export default for the main component representing a page:

import { FC } from 'react';

export const PDP: FC<Props> = props => {/* content */};

export default PDP; // <-- don't forget this

type ServerSideProps = {
  props: unknown;
};

export async function getServerSideProps(): Promise<ServerSideProps> {
  return { props: {} };
}

Special Page Components

There are also a couple of special pages: _app.tsx and _document.tsx. These represent fundamental components that wrap all of the others and are used to initialize the pages.

_app.tsx

_app.tsx represents the App as a whole and allows the following:

  • Persisting layout between page changes

  • Keeping state when navigating pages

  • Custom error handling using componentDidCatch

  • Inject additional data into pages

  • Add global CSS

_document.tsx

_document.tsx allows customizing the <html> and <body> for all pages. Other pages' contents appear within the <body> and so it is not accessible from them. You can add special script tags to the head here, such as for Google Tag Manager, so that they appear on all pages.

Modifying the contents of the page <head>

Next provides a Head component that allows a page to update the <head> such as providing the <title> and <meta name="description"> tags, which change for each page.

Client-Side Routing

Next provides a Link component to assist in routing transition on the client-side. This wraps a normal <a> tag and should be used to navigate between relative URLs on the site.

We’ve creating our own smart <Button> component for further convenience. If you pass it a URL, it will render as a link, otherwise a button.

// this
<Button to="/">Go Home</Button>;

// instead of this
<Link href="/">
  <a>Go Home</a>
</Link>
Tip
We also provide styled versions: PrimaryButton and SecondaryButton.
Note
We used to instead of href to avoid prop type conflicts with the underlying components.

In-App API Routes

Next.js allows defining API Routes. We make use of a few such routes under pages/api. Most are prefixed with /commerce as well to keep them distinct from Microservice endpoints when the app is proxied by a gateway.

  • /health provides a simple 200 OK response when heat for health checks.

  • /commerce

    • /accounts/current is used to manage the cache that stores the B2B account ID the user has selected to shop within.

      • Currently, we’re just creating an HTTP-only cookie to store the ID, and we use this to allow server-side requests elsewhere in the app to be able to access the account ID if needed.

    • /sandboxes/preview is used to support preview-on-site features with server-side-rendering by parsing the blSandboxPreview cookie and validating the preview token.

    • /tenants/resolution Handles interactions with the Tenant Resolution Endpoint to allow a server-side cache of the result so that page navigation or reloads don’t require re-resolving the tenant information.

      • The cache is set up in the custom Node server, see custom-server/server.js

General App Code

Everything but the components directly representing pages go into app/. This includes components, contexts, hooks, types, and utils.

These should be organized according to their domain first, then function.

Application Domains

To determine the domain we ask questions like:

  • Are they simple common components used in many places?

  • Do they have to do with a PDP only? Only Cart? Browse?

  • Are there a series of interconnected components, hooks, and utils like for dealing with CSR features?

For example, a component like ProductOptions that only appears on PDPs would be found under app/product/. However, consider a component called Header that renders the menu navigation, search input, account and cart links. Because it is part of the general layout of every page, it could go under its own domain of app/layout/.

Important
We prefer to not have domains depend on any others save common, and common should not have external dependencies.

Summary of Domains

  • auth

  • browse-and-search

  • cart

  • checkout

  • cms

  • common

  • csr

  • layout

  • my-account

  • product

Functional Classifications

After determining the domain, the second way we discriminate files is based on their functional classification.

  • Is this file a React component?

  • How complex of a component is it?

  • Is it a utility file or service that is used by components?

There are several functional distinctions that we can make based on the purpose of the file. Back to the ProductOptions component, that would further be placed under app/product/components, but a file called LocaleUtils would be found under app/common/utils.

Furthermore, a Header component can be broken down further such as with a HeaderMenu subcomponent. We can, then, have app/layout/components/header/header.tsx and app/layout/components/header/header-menu.tsx.

Summary of Functional Classifications

  • actions/: objects in the style of flux/https://redux.js.org/basics/actions[redux] actions and action creators

  • components/: contains renderable UI elements (see React docs for more info)

  • contexts/: custom React contexts

  • helpers/: functions like higher-order components and render-props or the more modern custom hooks

  • hooks/: custom React hooks

  • reducers/: functions in the style of flux/https://redux.js.org/basics/reducers[redux] reducers

  • services/: contains classes and objects responsible for performing more complex, often cross-cutting functions that are frequently not directly related to UI elements such as a authentication and site resolution

  • utils/: files that contain discrete utility functions for shared logic across components such as null-safe toString() or forEach() functions.

Localized Text

Static text should be placed in a messages.ts file and a react-intl MessageDescriptor should be made for each instance. We can store all of the messages for a domain in a single messages.ts instead of creating one per component. Thus, all product components could share one messages file at app/product/messages.ts rather than scattering the messages around.

Index Files

Each module should have an index.ts that exports all of the named components that may be referenced outside of that module. Thus, app/common/components should have an index.ts like the following:

// exports from sub-modules
export * from './form';

// exports from files
export * from './button';
export * from './formatted-amount';
export * from './icons';
export * from './simple-select';

This allows external modules like app/product to use short import statements when referencing common components

// this
import { Button, InputField } from '@app/common/components';

// instead of this
import { Button } from '@app/common/components/button';
import { InputField } from '@app/common/components/form/input-field';
Important
Module indices should have nothing but export statements: No code or logic of their own.

Test Files

These are grouped with the component, hook, or util they test and should have the same file name but ending in .test.ts or .test.tsx as appropriate.

Component Module Checklist

  • ✔︎ Component files that end in .tsx

  • ✔︎ Non-component files that end in .ts

  • ✔︎ File names that are kebab-case: table.tsx, table-util.ts, use-my-hook.ts

  • ✔︎ Files that contain export of same name:

    • table.tsx includes exported const Table = () ⇒ {}

    • use-my-hook.ts includes exported const useMyHook = () ⇒ {}

  • ✔︎ (Optional) Test files that end in test.ts or test.tsx as appropriate for content: Table.test.ts

  • ✔︎ messages.ts file with localization of static text with react-intl message descriptors for the entire module

  • ✔︎ index.ts that exports named exports for the entire module

    • ✔︎ No code, only exports

Example Directory and File Structure

app/
|_ auth/
|_ browse-and-search/
|_ cart/
|_ checkout/
|_ cms/
|_ common/
|    |_ components/
|    |    |_ form/
|    |    |    |_ input-field.tsx
|    |    |    |_ index.tsx (1)
|    |    |
|    |    |_ button.tsx
|    |    |_ index.tsx (2)
|    |
|    |_ contexts/
|    |_ hooks/
|    |_ utils/
|    |_ messages.ts (3)
|
|_ csr/
|_ layout/
|_ my-account/
|_ product/

pages/
|_ browse/
|    |_ [...url].tsx (4)
|_ checkout/
|    |_ confirmation.tsx
|    |_ index.tsx (5)
|    |_ information.tsx (6)
|    |_ payment.tsx
|    |_ review.tsx
|    |_ shipping.tsx
|
|_ _app.tsx (7)
|_ _document.tsx (8)
|_ index.tsx (9)
|_ cart.tsx (10)
  1. Exports all named exports from the forms sub-module: export * from './form';

  2. Exports all named exports from the components module like button and the form components

  3. Localized static text for everything in common

  4. Maps all urls starting with /browse/**

  5. Maps to the root checkout page at /checkout in the browser

  6. Maps to /checkout/information

  7. Custom App component, this wraps all pages and represents the app as a whole

  8. Customizes the <html> and <body> tags for all pages

  9. Maps to the homepage: /

  10. Maps to /cart

Other Modules of Note

Besides the main app/ and pages/ modules, you’ll see the following:

  • .vscode/: Shared Visual Studio Code settings go here that apply in everyone’s project

  • coverage/: Generated testing coverage reports go here

  • custom-server/: Contains code for running a custom Node server locally. This is primarily for when we need to run the

  • docs/: Project documentation

  • messages/: This contains files with the translations for all of the static text

  • public/: Static assets like images and the favicon.ico

  • results/: ¯_(ツ)_/¯ something for/from Jest

  • scripts/: Utility scripts for building or running the app or related resources

  • styles/: Stylesheets go here

  • test/: Global test configuration and mocks go here