Example of React directory structure pattern in 2021

Example of React directory structure pattern in 2021
Photo by Simone Hutsch / Unsplash
💡
Date of writing is 2022-11-01

In 2021, I created a website using Next.js and would like to share the structure I used.

There are many opinions about directory structure, and each of us has a strong opinion. I do not intend to say that this is the best way, but rather to use it as a pattern for reference.

The background of this project is the following elements.

  • Small startup with only a few engineers, fast-paced environment
  • Possibility of discarding/changing things because they are not PMF'd

Directory structure and policies

We made a decision not to include architecture as much as possible in this project as a premise.

The purpose of this is to "lower the barrier to entry for new members". Various people gave me advice early in the project, and I used the ideas I heard at that time in their entirety.

Specifically, we did not use architectural theories such as DDD, and our directory structure was a general structure + feature directory (+storybook).

A treasure trove of React best practices! The "bulletproof-react" is too instructive

It is close to official React documentation, but we did not define features strongly conceptually. Instead, we always defined the ubiquitous language within that team and used that word as common knowledge, which resulted in the frequent occurrence of that word in the features. (Similar to the Domain Driven Development attitude, too)
Because the team was remote and asynchronous, rules were clearly defined through means like eslint.

React+TSプロジェクトで便利だったLint/Format設定紹介
eslint-plugin-strict-dependencies
ESlint plugin to define custom module dependency rules.. Latest version: 1.2.3, last published: 21 days ago. Start using eslint-plugin-strict-dependencies in your project by running `npm i eslint-plugin-strict-dependencies`. There is 1 other project in the npm registry using eslint-plugin-strict-dep…

The rest of the code is very loose, and I tried to keep the style as speedy as possible.

I tried to work in a tentative style, and only think of problems when they arise. (WebStorm's refactoring feature allows refactoring with almost no bugs even if changes are made later...)

directory

Incidentally, two patterns of configuration are officially introduced: "grouping by function/root" and "grouping by file type".

ファイル構成 – React
ユーザインターフェース構築のための JavaScript ライブラリ

src

This time, the src directory is similar to [bulletproof-react](https://github.com/alan2207/bulletproof-react/blob/master/docs/project-structure.md).

The directory directly under src is for application-wide dependencies, and the rest is for features.

src
|
+-- components        # Common components

+-- constants         # Globalc constants
|
+-- features          # Feature based modules
|
+-- hooks             # Utilities
|
+-- lib               # Generic functions that depend on NPM library. wrapper, etc.
|
+-- pages             # because we adopted Next.js.
|
+-- routes            # routes configuration
|
+-- scheme            # zod scheme etc.
|
+-- styles            # global style, color(include chakra's theme etc..)
|
+-- types             
|
+-- utils             

State Management is

  • Global management: Context
  • Partial management: Recoil

The store management is now based on Context and Recoil.

At first, we used only Context to manage the store, but we wanted to manage state by function, and we felt it was time-consuming to implement Context each time, so we introduced Recoil to reduce the amount of code and ease of use.

Context is placed in `src/components/contexts`, but it might have been better to place it in `src/stores` because it is not so easy to see from the root (src).

features

src/features/awesome-feature
|
+-- api         # APIアクセスする主にPromise関数
|
+-- hooks       # feature依存hooks: apiファイルやformのstate管理など
|
+-- state       # 機能に依存するstate(Recoil)
|
+-- (etc)       # その他、機能に関するものすべて. converterなど

All feature-related senses are included.

I placed everything related to features in the components directory, including API/hooks and state.
To keep feature-specific parts organized, the directory named features can be found inside the components directory.

components

src/components
|
+-- context                  # 全体store. Context
|
+-- features/awesome-feature # 機能依存のComponents: 主にForm.tsxなどでfeaturesのhooksを呼び出す
|
+-- pages                    # Next.js(~v12)でpageコンポーネントを分割するため
|
+-- ui                       # UIパーツを配置。基本的にstorybookで構築できるようなプリミティブな値を引数にする。

The team made an effort to prevent contamination of loosely-coupled directories. To achieve this, they placed items that didn't look like function names, gray-area items that span multiple functions, or items that depend on functions but may be called in common in src/components/features/xxx/.

Firebase has been implemented in our project for better performance and faster implementation. To achieve this, we have enabled direct reading of react-firebase-hooks from the component and also allowed direct reading of src/components/features/xxx/ from the component. Additionally, we have placed react-firebase-hooks in src/components/features to further optimize performance.

components/ui

src/components/ui
|
+-- (parts name)       # 各UIパーツ
|
+-- layouts            # FooterやHeaderなどパーツを組み合わせたもの

Based on my understanding, I believe that it is more practical to approach Atomic Design by dividing it into distinct parts. In my opinion, it would be better to start with Atoms and then move on to writing the story. After that, Molecules and above, which are a combination of Atoms and Molecules, can be divided into separate parts. Finally, the higher-level components can be further divided into additional parts.

At the beginning of the design process, we clearly defined the parts and decided that anything beyond that would be determined during screen design, such as using the same structure as the screen. This approach worked well for our team's operation.


The result of my thinking, the primary dependencies of the constituent files of a given page are as follows.

Here is the rule for that dependency in eslint-plugin-strict-dependencies (assuming you have set the path in tsconfig)

// .eslintrc.json
{
	"strict-dependencies/strict-dependencies": [
      "error",
      [
        /**
         * Example:
         * Limit the dependencies in the following directions
         * pages -> components/page -> components/ui
         */
        {
          "module": "@/components/page",
          "allowReferenceFrom": ["@/pages"],
          "allowSameModule": false
        },
        {
          "module": "@/components/ui",
          "allowReferenceFrom": ["@/components/pages", "@/components/features"],
          "allowSameModule": false
        },

        /**
         * example:
         * Disallow to import `next/router` directly. it should always be imported using `libs/router.ts`.
         */
        {
          "module": "next/router",
          "allowReferenceFrom": ["src/lib/router.ts"],
          "allowSameModule": false
        },
        {
          "module": "@/*",
          "allowReferenceFrom": ["src/*"],
          "allowSameModule": false
        }
      ],
      // options
      {
        "resolveRelativeImport": false
      }
    ]
}
// tsconfig.json
{
	...
  "compilerOptions": {
    "baseUrl": "./",
    "paths": {
      "@/*": ["src/*"]
    }
  }
	...
}

The concept is to maintain separation between the endpoints of the layers and the logic, and to simply determine the location of the intermediary layer. As the intermediary layer lacks knowledge of the hierarchy, the assumption is that if the endpoints are stabilized, the intermediary layer can be inserted in between them without any issues.

It's a bit outdated, but I hope it helps!

Satoshi Nitawaki

Satoshi Nitawaki

App dev, tech, and life https://bento.me/nita
Tokyo, Japan