17
"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.
First, you can learn about the directory structure, which tends to vary from project to project.
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.
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/*/*'],
},
],
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
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;
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="." /> },
],
},
];
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.
The next section is about component design.
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>
);
};
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 :)
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 ofextends T<unknown>
such asTFormValues extends Record<string, unknown> = Record<string, unknown>
is a typedef tip that I often use for puzzles.
- I'm working hard on the type definitions in
- It may be that the number of re-renders is less than my design plan? (untested).
- since registration is executed in the Form layer as
In addition, it satisfies all the advantages of the idea I was designing, so I thought it was completely upward compatible (great).
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.
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
- testing-library is also via
- 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...
- I didn't set up
- 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 sinceReactNode
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.
- I've been using
- Naming
- https://github.com/kettanaito/naming-cheatsheet I've never heard of such a repository. I can use it as an internal README.
- Overall, I like the selection of libraries (this is completely subjective).
- tailwind
- react-hook-form
- msw
- testing-library
- clsx
- On the other hand,
react-helmet
is almost out of maintenance, andreact-helmet-async
should be better, so I published a pull request (https://github.com/alan2207/bulletproof-react/pull/45 )
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.
17