7.4 KiB
Introduction
Directory Structure
We categorize files based on the type of functionality they provide. We group them into directories at the root of the source code directory on the project/module level(e.g., /src/*, /src/modules/some-module/*).
We separate code files into 7 directories common across all of our React projects: containers, components, layouts, styles, types, lib, and utils. You may also have other categories defined within your project's scope(e.g., pages, stores, configs, constants, data, and models.)
In the following section, we'll explain the aforementioned common categories in detail.
Common Structure
📁 components/
Components are reusable and styled React components agnostic to the project's data structure. They must not make any API calls or directly read query responses; instead, data and the callbacks needed by a component must be passed down by a parent component. The only global state or variables they may access are provided for styling purposes.
✔️ What a component can DO:
- Render other components.
- Access theme and CSS variables.
- Use utility functions and hooks.
❌ What a component can NOT do:
- Access global states.
- Render containers.
- Make network calls.
Place a component file inside a directory with the same name as the component in PascalCase. Then, create an index file inside that directory and export the component and its property type:
├── components
| ...
│ ├──Button
│ │ └── index.ts # exports { Button, ButtonProps }
│ │ ├── Button.tsx # exports { Button, ButtonProps }
│ │ ├── Button.module.scss
| ...
📁 containers/
Containers are used to provide global states and APIs through React contexts. They may render a composition of components or other containers, and are responsible for handling state logic, making network requests, etc. There's no restriction on containers having styles and providing layout.
✔️ What a container can DO:
- Anything a component can do.
- Render other containers.
- Read, modify, and provide global states.
- Make network calls.
❌ What a container can NOT do: Nil.
The structure of containers
directory is the same as that of components
directory.
├── containers
| ...
│ ├── LoginPage
│ │ └── index.ts # exports { LoginPage, LoginPageProps }
│ │ ├── LoginPage.tsx # exports { LoginPage, LoginPageProps }
│ │ ├── LoginPage.module.scss
| ...
📁 layouts/
Layouts are React components in which pages will be rendered. Layout components must be rendered by and registered in the same top-level component that renders pages.
The directory structure is similar to the components
and the containers
, except the name of the file containing the layout component, must consist of the name of the layout — not the component — followed by a .layout<.ext>
suffix; like:
├── layouts
│ ├── DefaultLayout
│ │ └── index.ts # exports { DefaultLayout, DefaultLayoutProps }
│ │ ├── Default.layout.tsx # exports { DefaultLayout, DefaultLayoutProps }
│ │ ├── DefaultLayout.module.scss
│ ...
📁 styles/
Files living in the styles directory contain global styles, variable definitions, and utility functions.
Below is an example of the styles
directory:
├── styles
│ ├── _animate.scss # Contains CSS animation mixins.
│ ├── global.scss # Contains global styles.
│ ├── _utils.scss # Contains common SCSS utility mixins(e.g., responsive, typography, etc.)
│ ├── _vars.scss # Contains SCSS variable definitions such as breakpoints, font sizes, etc.
| ├── ...
📁 types/
A type file contains custom local Typescript type declarations for the project/module. The declarations are mostly derived from external types — like custom query results — and will be used mostly by the containers.
The declarations must be defined within a Typescript namespace, and the name of the namespace must be the same as the file's name in PascalCase followed by a Types
suffix.
A type's file name is defined in camelCase with a .types
suffix followed by the file's extension. Below is an example of the types
directory:
├── types
│ ├── auth.types.ts # exports { AuthTypes }
│ ├── product.types.ts # exports { ProductTypes }
| ├── ...
📁 lib/
The lib
folder contains the project's or the module's local libraries, like API clients, queries, workers, i18n, database connectors, etc.
📁 utils/
Utilities are mainly small functions and are decoupled from and agnostic to the project's data structure and should be portable to other projects with minimum effort. In most cases, they're React hooks(e.g., useWindowSize
, useElementSize
, useTranslator
, useFileInput
, etc).
Below is an example of the utils
directory:
├── types
│ ├── fileInput.utils.ts
│ ├── useBlockRouteChange.util.ts
│ ├── useDisableScrollbar.util.ts
│ ├── useFileInput.util.ts
│ ├── useWindowSize.util.ts
│ ...
Modules
As in big projects, the root directories can get easily bloated and messy, and thus, hard to read and develop, we break the project into multiple modules based on features, with each module having the common structure described in the previous section(having directories like containers
, components
, styles
, etc.) A module named common
will contain the project's shared code like basic components, hooks, API client, etc.
In addition, we use Typescript alias paths for shorter and a more clear import statements:
// SomeComponent.tsx
import { Button } from '@common/components/Button'
// tsconfig.json
{
"compilerOptions": {
"paths": {
"@common/*": ["./src/modules/common/*"],
}
}
}
Data Fetching
We use React Query for client-side data fetching.
State Management
We use Hookstate.js — and in some cases together with React context — to manage global states, as it's very minimal and yet it comes with a powerful and flexible API. Additionally, we use it when a component involves dealing with a large state, as Hookstate empowers Proxies to track nested state changes to ensure only necessary components will rerender (Read more here). However, in the cases of complex and heavily event-driven applications, we use Rematch with Redux-Saga as an alternative solution.