"bulletproof-react" is a hidden treasure of React best practices!

The GitHub repository "bulletproof-react", which is published as an example of React application architecture, is very informative and I will share it with my own views.

Directory structure

First, you can learn about the directory structure, which tends to vary from project to project.

Put the source code under src.

In bulletproof-react, React-related source code is stored under the src directory; conversely, there are no directories such as components or utils in the root directory.

For example, the default application created by Create Next App has source code directories such as pages in the root directory, so putting them under src is the intentional directory structure of this repository.

The root of a real project will have a mix of markdown documentation (docs), CI settings such as GitHub Actions (.github), and Docker settings (docker) if the application is container-based. Therefore, if we put components directly at the root level, the source code of the application and the non-components will be mixed in the same hierarchy.

Not only is this confusing, but it is also convenient to unify the source code under src when writing CI settings, for example, to make it easier to specify the scope of application.

features directory

An interesting point in the directory structure of this repository is the features directory.

It contains the following directories:

src
|-- assets
+-- assets # assets folder can contain all the static files such as images, fonts, etc.
*(omitted)*
+-- features # feature based modules ← here
*(omitted)*
+-- utils # shared utility functions

Under features is directories with the name of each feature that the application has. For example, for a social networking service, this might be posts, comments, directMessages, and so on.

src/features/awesome-feature
|
+-- api # exported API request declarations and api hooks related to a specific feature
|
+-- components # components scoped to a specific feature
*(omitted)*
+-- index.ts # entry point for the feature, it should serve as the public API of the given feature and exports everything that should be used outside the feature

When deciding on a directory, it is important to consider what criteria to use. You tend to decide the directory name based on what role the module plays from the engineer's point of view. You may have components, hooks, types, etc. under src, and then finally create a directory for each function in each directory.

I myself create a directory called app/Domain for backend implementations, and then create a directory for each feature, such as app/Domain/Auth or app/Domain/HogeSearch. So it made a lot of sense to manage the front-end with the same idea.

By creating a features directory, you can manage components, APIs, Hooks, etc. by feature. In other words, if you have an API for each feature, you can cut the directory for the API, and if you don't, you don't have to.

Also, if you are running a service, you often want to discontinue a feature, but you only need to delete the corresponding directory under features.
I thought this was a great idea, because there is nothing worse than having unused features lingering around like zombies.

Creating a directory for each feature will also help to speed up the verification of the business side.
If the directory is divided by features/HOGE as in this repository, it is possible to prioritize development speed with a fat design in the initial release, and impose strict constraints in the second and subsequent releases.

You can decide whether a file should be placed under features or not, based on whether it will disappear with the feature when the feature is obsolete.

You can also write ESLint rules to prohibit dependency of features -> features.

'no-restricted-imports': [
          'error',
          {
            patterns: ['@/features/*/*'],
          },
        ],

Place modules that are needed across features under src/HOGE.

Components that are used across features, such as simple button elements, should be placed under src/components.

e.g. src/components/Elements/Button/Button.tsx

providers and routes directories are smart.

When I'm writing React and React Native applications, I often write both Provider and Route settings in App.tsx, and the number of lines gets bloated, but I found it very clever that this repository has separate providers and routes directories.

As a result, the contents of App.tsx are very simple. I would like to copy this.

import { AppProvider } from '@/providers/app';
import { AppRoutes } from '@/routes';

function App() {
  return (
    <AppProvider>
      <AppRoutes />
    </AppProvider>
  );
}

export default App;

Already support the implementation of react-router@v6 assumption.

In v6 of React Router, new features such as <Outlet> can be used to carve out routing into a separate object.

This repository (at the time of writing, it is dependent on the beta version, so there may be minor changes in the future) already contains the following implementation examples, which I think can be used for preliminary study.

export const protectedRoutes = [
  {
    path: '/app',
    element: <App />,
    children: [
      { path: '/discussions/*', element: <DiscussionsRoutes /> },
      { path: '/users', element: <Users /> },
      { path: '/profile', element: <Profile /> },
      { path: '/', element: <Dashboard /> },
      { path: '*', element: <Navigate to="." /> },
    ],
  },
];

Supplementary information: Other examples of directory structure

I am currently managing a structure similar to the idea of the following article, rather than the idea of aggregating into features.

The model in this article is similar to the features in this repository. The general idea is to put all the .tsx files under components, which is well known from the default structure of Nuxt.js, so creating a directory components/models and putting components for each feature under it is also a good idea.

Component Design

The next section is about component design.

Create components in-house that wrap components from external libraries.

This design pattern is called the Anti-Corruption Pattern. I have already worked on it myself and recommend it.

By simply using a component that wraps the <Link> of react-router-dom, as shown below, I can increase the possibility of limiting the scope of influence when destructive changes are made to that component in the future. If you are importing external libraries directly from a number of components, you will be affected, but if you have in-house modules in between, you will have a better chance of limiting the impact.

In fact, it's hard to make it work for all of them, but it's useful to keep it in mind.

import clsx from 'clsx';
import { Link as RouterLink, LinkProps } from 'react-router-dom';

export const Link = ({ className, children, ...props }: LinkProps) => {
  return (
    <RouterLink className={clsx('text-indigo-600 hover:text-indigo-900', className)} {...props}>
      {children}
    </RouterLink>
  );
};

There are many examples of implementations using the Headless component library.

Headless UI is a UI library that can be unstyled or easily overridden, and is only responsible for state retention, accessibility, etc. React components nowadays can take on all the styling, a11y, state, and communication, so a library with this kind of separation of thought is a very smart approach.

Incidentally, the same README says that for most applications, Chakra with emotion is the best choice. I also think that Chakra is currently the best component library, and MUI is the next best, so I rather agree with the statement :)

A design example using react-hook-form

There is a Form library based on the premise of the heyday of Hooks called react-hook-form (RHF). I personally recommend it.

In this repository, RHF is embedded using a wrapper component called FieldWrapper. The idea is to implement a form component by putting <input> etc. in the FieldWrapper.

import clsx from 'clsx';
import * as React from 'react';
import { FieldError } from 'react-hook-form';

type FieldWrapperProps = {
  label?: string;
  className?: string;
  children: React.ReactNode;
  error?: FieldError | undefined;
  description?: string;
};

export type FieldWrapperPassThroughProps = Omit<FieldWrapperProps, 'className' | 'children'>;

export const FieldWrapper = (props: FieldWrapperProps) => {
  const { label, className, error, children } = props;
  return (
    <div>
      <label className={clsx('block text-sm font-medium text-gray-700', className)}>
        {label}
        <div className="mt-1">{children}</div>
      </label>
      {error?.message && (
        <div role="alert" aria-label={error.message} className="text-sm font-semibold text-red-500">
          {error.message}
        </div>
      )}
    </div>
  );
};

I have been discussing design patterns using RHF for a long time, and have published a practical example of component design in the following article.

The design philosophy presented here was to separate the layers as View layer←Logic layer←Form layer.

On the other hand, here is a list of the relative merits of designing with wrapper components in this repository, as perceived at a quick glance.

  • The label and error display, which should be common to all form components, can be standardized.
    • In my design, labels and error messages are handled by either the View layer or the Form layer, so they are not common. It is necessary to implement them separately.
  • No need to use useController.
    • since registration is executed in the Form layer as registration={register('email')}.
    • In addition, the argument string of the register method is type-safe.
      • I'm working hard on the type definitions in Form.tsx to make this type-safe.
      • For example, I've adopted the design concept of wrapping View layer as HOC, but I couldn't define the type well without applying some any.
      • The use of unknown in the form of extends T<unknown> such as TFormValues extends Record<string, unknown> = Record<string, unknown> is a typedef tip that I often use for puzzles.
    • It may be that the number of re-renders is less than my design plan? (untested).

In addition, it satisfies all the advantages of the idea I was designing, so I thought it was completely upward compatible (great).

Error handling

For error handling in React, react-error-boundary is useful.

It may be appropriate to use it in AppProvider.tsx as mentioned above.

<ErrorBoundary FallbackComponent={ErrorFallback}>
        <Router>{children}</Router>
      </ErrorBoundary>.

I was personally impressed with the behavior of the Refresh button specified in the component for fallback.

<Button className="mt-4" onClick={() => window.location.assign(window.location.origin)}>
        Refresh
      </Button>.

What window.location.assign(window.location.origin) is doing here is transitioning to the top page because it is transitioning to the origin. When I saw this, I thought that I should just write location.reload(), but I realized that if I want to put a button on the top page, it would be more appropriate to go back to the top page, because it will keep dropping infinitely when an error occurs due to Invalid query parameter or page.

You can also use location.href = to get the same behavior, but assign has the subtle advantage that it is a method call and therefore easier to write tests for, so assign is slightly preferable.

Incidentally, from a personal point of view, I thought it would be better to use location.replace(), which doesn't leave the error in the history, because it seems to be more subtle if you want to return to the page where the error occurred. However, I wonder if that would result in unexpected behavior.

Other

There are many other things that I noticed, but I'll just list them here, instead of reading the Markdown under docs in the repository for details.

  • The source code scaffolding tool is also set up.
    • With Scaffolding, you can generate files of a certain format in a target directory with a single command.
    • It is set up under the generators directory.
    • This is possible because the directory structure is stable.
    • https://www.npmjs.com/package/plop is used
    • By the way, I like Scaffdog, which can be written in markdown.
  • The test code setup is also massive
    • testing-library is also via test/test-utils.ts as a corruption prevention layer
    • The setup of MSW is also very thorough
    • I know MSW is useful, but I hadn't imagined what it would look like after it was set up, so it's very helpful.
    • Already integrated with GitHub Actions
  • Performant.
    • The basic but important point is that the page components are lazyImported in the Route file, so the code is split.
    • I was wondering why React.lazy can only be used for Default Export, but I heard that it can be used for named export. I didn't know that (or I never thought to do something about it).
    • https://github.com/alan2207/bulletproof-react/blob/master/src/utils/lazyImport.ts
    • I've also made it possible to record web-vitals.
  • About ESLint
    • I didn't set up import/order because I thought it would be too radical, but now that I've seen it set up, it does seem to be easier to read...
  • Type ReactNode is safe to use.
    • I've been using ReactNode for all React element props, but I was wondering if I need to be more strict since ReactNode can be classified into more detailed types. I was wondering if I should do that.
    • Of course, there are times when you should do that, but I'm glad to know that ReactNode is fine for most cases.
  • Naming
  • Overall, I like the selection of libraries (this is completely subjective).

Summary

I've never seen a template repository with such a thorough and complete set of Production Ready configurations. Personally, I'd like to refer to it regularly as a bookmark because it contains many things I know but haven't used, such as Storybook and Cypress.

I also think vercel/commerce is a good place to learn, but if there are any other repositories you'd recommend, please let me know!

There are a lot of things that I haven't kept up with in the React projects I write on a regular basis at all, but I'd like to keep up with them, judging the need on a case-by-case basis.

16