Broadleaf Microservices
  • v1.0.0-latest-prod

Component Best Practices

Code Organization

Organizing Imports

For the sake of consistency and readability, imports are ordered as follows:

  1. React

  2. Next

  3. Node

  4. Other 3rd party imports in alphabetical order

  5. Broadleaf libraries (e.g., CommerceSDK)

  6. Newline

  7. Imports from other domains (e.g., @app/common/, @app/product/) and functional classifications (e.g., actions/, hooks/)

    • In alphabetical order by domain → then function → then name

    • These should always start with the @app/

  8. Newline

  9. Imports from the same functional classification (/hooks) or domain (/product)

    • These can use file path shorthand, e.g., ../ for parent directory

  10. Imports from the same module

    • These should also use file path shorthand, e.g., ./ for current directory

  11. Newline

  12. Localized messages

We then use alphabetical ordering when importing multiple components/functions from the same dependency: import { get, has, isEmpty, map } from 'lodash';

Example
import { useState } from 'react';
import { useRouter } from 'next';
import type { IncomingMessage } from 'http';
import classNames from 'classnames';
import { get, has, isEmpty, map } from 'lodash';
import { BrowseClient } from '@broadleaf/commerce-browse';

import { Dropdown } from '@app/common/components';
import { useFormatMessage } form '@app/common/hooks';

import Menu from '../Menu';

import messages from './messages';

We also prefer as an import path as possible, usually no more than 3 path segments. This will require that our exports are done correctly.

// 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';

Organizing Exports

Our rules for export statements:

  • Anything that should be exported, whether component, function, type, or constant, should be marked with the export keyword when they are declared rather than after the component or at the bottom of the file, example below.

  • Functions that should be exported should use the arrow syntax

  • Only pages should have default exports to keep index.ts files clean, example below

  • Each module should have an index.ts that has export * from './some-file'; so that all named exports are exported.

component.tsx example
import { FC } from 'react';

import { IMG_NOT_FOUND } from '@app/common/utils';

// exported type
export type Asset = {
  altText: string;
  contentUrl: string;
  tags?: Array<string>;
  title?: string;
  type: string;
};

type Props = {
    asset: Asset;
};

// exported component
export const Asset: FC<Props> = props => {
  const { asset } = props;
  return (
    <img
      alt={asset.altText}
      src={getContentUrl(asset)}
      title={asset.title}
    />
  );
}

// exported function
export const getContentUrl = (asset: Asset): string => {
  return asset.contentUrl || IMG_NOT_FOUND;
}
Corresponding index.ts example
// this exports Asset, Component, and getContentUrl all in one line
export * from './component';

We Prefer Functional Components to Class Components

Functional components take the form of

const Button = ({ label }) => <button type="button">{label}</button>;

Class components take the form of

class Button extends React.Component {
  render() {
    return <button type="button">{label}</button>;
  }
}
Functional components:
  • let you use the hooks API, which keeps your write cleaner, more readable, and more compact code.

  • encourage maintaining less state and hooking into the lifecycle API less, which increases reusability

  • encourage reducing component size and complexity and splitting them into smaller components instead

Also note that the very far-future goal of the React team is deprecate classes altogether—the point being the React team wants you to prefer functional components.

Using and Adding Icons

Available icons are listed in app/common/components/icons.tsx. These are all SVGs and are sourced from https://heroicons.com/. See their site if you want to add a new one; then hover over the icon you want and select "Copy JSX". You can then paste this in app/common/components/icons.tsx and only have some minimal massaging to do to make it uniform with the other icons.

Example icon
export const MenuIcon: FC<IconProps> = ({
  className = 'w-6 h-6',
  stroke = 1,
  ...props
}) => (
  <Svg
    className={className}
    fill="none"
    stroke="currentColor"
    viewBox="0 0 24 24"
    {...props}
  >
    <path
      strokeLinecap="round"
      strokeLinejoin="round"
      strokeWidth={stroke}
      d="M4 6h16M4 12h16M4 18h7"
    />
  </Svg>
);
Tip
Icon size is primarily controlled with the className prop, although stroke controls the thickness of the vector paths.

Debugging Tips

Use console statements such as console.debug to print data or add debugger; on a line to set a manual breakpoint. The benefit of console.debug over console.log is that we have configured a precommit hook to flag any attempts to commit console.debug. Only use console.log for logs you want in production, not for debugging purposes.

Use React’s FC type as the type for components

Since we’re using Typescript, we need to define the prop and return types. A convenient way to do that for components is to create a custom type for the props then combine that with FC (for FunctionalComponent) on your component definition:

Example icon
import { FC } from 'react';

type IconProps = {
  className?: string;
  stroke?: number;
  title?: string;
}

export const MenuIcon: FC<IconProps> = ({
  className = 'w-6 h-6',
  stroke = 1,
  ...props
}) => (
  <Svg
    className={className}
    fill="none"
    stroke="currentColor"
    viewBox="0 0 24 24"
    {...props}
  >
    <path
      strokeLinecap="round"
      strokeLinejoin="round"
      strokeWidth={stroke}
      d="M4 6h16M4 12h16M4 18h7"
    />
  </Svg>
);

Next provides a router hook for declaratively navigating between pages hosted by the app, and a Link component instead of a plain <a>.

Important

useRouter provides several callbacks for declarative navigation. However, those methods cannot be called on the server-side. If you need to call them early in the page render like in the example below, make sure the page is not server-side rendered.

Example disabling SSR for a page
import dynamic from 'next/dynamic';

const MyPageComponent = () => {};

export default dynamic(() => Promise.resolve(MyPageComponent), { ssr: false });

Alternatively, try doing the redirect on the server-side without using useRouter.

Example redirect on server-side
import { GetServerSideProps } from 'next';

export default function RedirectingComponent(): JSX.Element {
  return null;
}

export const getServerSideProps: GetServerSideProps = async () => {
  return {
    redirect: {
      destination: '/',
      permanent: false,
    },
  };
};
Example using router
import { FC } from 'react';
import { useRouter } from 'next/router';

export default function SignInOrRegister(): JSX.Element {
  const { replace } = useRouter();

  if (isAuthenticated) {
    replace('/my-account');
    return null;
  }
}
Note
Usually you should use the Button component instead, which can dynamically decide between showing a button or a link depending on if a URL is provided. It’s also a bit simpler since you don’t also need to add a <a> inside of the Link.
Example with Link (some details omitted for brevity)
import Link from 'next/link';
import { get, isEmpty, omit, pick } from 'lodash';

export const Button: FC<ButtonProps> = props => {
  const { children, ...rest } = props;

  if (!isEmpty(get(rest, 'to'))) {
    const linkProps = pick(rest, linkFields);
    const anchorProps = omit(rest, linkFields);
    return (
      <Link {...linkProps} href={linkProps.to}>
        <a {...anchorProps}>{children}</a>
      </Link>
    );
  }

  return <button {...(rest as BtnProps)}>{children}</button>;
};

const linkFields = [
  'to',
  'href',
  'as',
  'replace',
  'scroll',
  'shallow',
  'passHref',
  'prefetch',
  'locale',
];