Various React applications require various structures. Let's define a set of advice on how to think about project organisation and apply it to a medium-sized project.
These recommendations aren't a silver bullet for every project. Apart from the "casual one", which is described below, library-structured monorepo projects, micro frontends, etc., have their own issues and requirements. Each project needs to consider complexity, future development, available budget and other aspects.
Let's discuss a project that could become a medium-sized app with hundreds of components. Consider Next.js, AppRouter, without workspaces and a comfortable budget:
define potential issues to be avoided or reduced,
define axioms, rules and suitable design patterns,
go through different use cases with implementation.
Issues to be avoided or reduced
Variables/functions/components are imported from "random" places in the project, which makes it hard to understand which parts of the app may be affected by changes.
An unreasonable number of files in one directory makes the whole project messy.
Exporting everything from everywhere makes it unclear which components can be used without context (e.g.
NavItem
is dependent onNav
).Too high or too low granularity in terms of code per file.
Expectations
A project can contain hundreds of pages, and it is obvious where components are used.
Folder structure may be used with or without Next.js.
Understandable for juniors - a clear structure of folders, files and file naming.
Design patterns & concepts
Component-based principle – components emphasise a separation of concerns and distribute themselves as a black-box functionality with the possibility to extend.
Component High cohesion – a component has everything in one place (its file/folder) except shared dependencies or special cases.
Compound pattern – a JSX component that has its own optional structure (e.g.
Card
,CardBody
) and uses a compound structure (e.g.Card
,Card.Body
) to prevent confusing exports.
Structural principles
Application code related to the functionality is inside
./src
folder aliased by@/
path (src root)._(.*)
are special Next.js folders,_
could be avoided without the framework.@/app
is the AppRouter folder containing pages, see Next.js docs.@/**/*
files could be divided into 2 groups - React components and "secondary variables" (= constants, utils, hooks, ...).
ReactComponent is a file or folder having exactly the same interface/behaviour as a file. Both export 1 main component ± secondary variables.
File –
ComponentName.tsx
contains exactly 1 React component and may have secondary stuff. Everything uses named export. However, secondary variables cannot be used without a relation to the component. Otherwise, see Shared Code and Implementation below.Folder –
ComponentName
has a similar interface as a file – exposes 1 React component and secondary variables viaComponentName/index.ts
so that it can replace a fileComponentName.tsx
without redefining imports across a project. Besides the component itself, inside the folder, we can define other files/folders that are required for its instance. All imports within the folder are relative.
Shared Code – everything that needs to be used in multiple parts of the project.
Component-related – secondary variables can exported from the file/folder with the main component. These variables should be used only with this component. E.g.
Nav
also providesNavItem
, or some special hooks.Shared between ReactComponents – the shared variable is separated into a stand-alone file, placed into
_(.*)
folder according to its type and put into the nearest common parent folder. Since separation, all imports of this variable should be absolute@/
.Global modules – some items expected to be used everywhere – typically form fields, buttons, auth hooks, GraphQL Fragments, etc. They are placed in root folders
./src/_(.*)/
according to their type.
The project follows the naming conventions described in the cheat sheet.
Implementation
Let's assume that we need to create a blog page. We start with one file and extend a structure step by step.
Pages
Let's begin with a homepage – a list of articles. URI http://localhost/blog
, a page file always has page.tsx
filename. We assume that routes are auto-detected by Next.js or manually declared with a router library.
./src
└── app
└── blog
└── page.tsx
URI http://localhost/blog/article/unique-slug
contains chosen article:
./src
└── app
└── blog
├── articles
│ └── [slug]
│ └── page.tsx
└── page.tsx
Pages use default export - Next.js requirement and React Router lazy loading compatibility.
// src/app/blog/page.tsx
const BlogPage = () => <></>;
export default BlogPage;
Simple React component
The blog page renders multiple ArticlePreview
components. An article is rendered by Article
component.
./src
└── app
└── blog
├── _components
│ └── ArticlePreview.tsx
├── articles
│ └── [slug]
│ ├── _components
│ │ └── Article.tsx
│ └── page.tsx
└── page.tsx
All components use named export, which ensures the same component name within an application.
// src/app/blog/articles/[slug]/_components/Article.tsx
export const Article = () => <></>;
Shared React component
We discovered that ArticlePreview
and Article
need to use the same UI component Rating
- let's find the nearest parent folder of both components and create Rating.tsx
inside type-related folder (_components
).
./src
└── app
└── blog
├── _components
│ ├── ArticlePreview.tsx
│ └── Rating.tsx
├── articles
│ └── [slug]
│ ├── _components
│ │ └── Article.tsx
│ └── page.tsx
└── page.tsx
Rating
is an independent component. For ArticlePreview
it is a sibling node within /blog/_components
folder (main component inside page.tsx
) – relative import. For Article
it is a component in the upper structure – absolute import.
// src/app/blog/_components/ArticlePreview.tsx
import { Rating } from './Rating';
export const ArticlePreview = () => <Rating />;
// src/app/blog/articles/[slug]/_components/Article.tsx
import { Rating } from '@/app/blog/_components/Rating';
export const Article = () => <Rating />;
Complex React component
Article
seems to be too long and needs to be divided into smaller parts – ArticleHeader
, ArticleBody
. Both of them cannot be used without Article
.
At first, we transform the file component into a folder component.
./src
└── app
└── blog
└── articles
└── [slug]
└── _components
└── Article
├── Article.tsx
└── index.ts
// src/app/blog/articles/[slug]/_components/Article/index.ts
export { Article } from './Article';
Interfaces were preserved. Then we add Article
-related components.
./src
└── app
└── blog
├── _components
│ ├── ArticlePreview.tsx
│ └── Rating.tsx
├── articles
│ └── [slug]
│ ├── _components
│ │ └── Article
│ │ ├── Article.tsx
│ │ ├── ArticleBody.tsx
│ │ ├── ArticleHeader.tsx
│ │ └── index.ts
│ └── page.tsx
└── page.tsx
Usually, the main component should be the only one exported from the folder. In case that ArticleHeader
and ArticleBody
need to be used outside Article
, we may export them with the Compound Pattern (recommended way) or directly from the Article
folder:
// src/app/blog/articles/[slug]/_components/Article/index.ts
export { Article } from './Article';
export { ArticleBody } from './ArticleBody';
export { ArticleHeader } from './ArticleHeader';
In this case, we need to handle them as strictly bounded and not use them without Article or, even worse, inside another React Component as a shared one, as we do with Ratings
.
Complex React component - Compound Pattern
To prevent exporting useless (on their own) components:
./src
└── app
└── blog
└── articles
└── [slug]
└── _components
└── Article
├── Article.tsx
├── ArticleBody.tsx
├── ArticleHeader.tsx
└── index.ts
// src/app/blog/articles/[slug]/_components/Article/Article.tsx
import { Rating } from '@/app/blog/_components/Rating';
import { ArticleBody } from './ArticleBody'
import { ArticleHeader } from './ArticleHeader'
export const Article = () => <Rating />;
Article.Body = ArticleBody;
Article.Header = ArticleHeader;
// src/app/blog/articles/[slug]/page.tsx
import { Article } from './_components/Article';
const Page = () => (
<Article>
<Article.Header />
<Article.Body />
</Article>
);
export default Page;
Additional secondary items (hooks, validations, etc.)
ArticleHeader
needs to have a useColoredPart
hook. It isn't shared with Article
and belongs only to the ArticleHeader
. ArticleHeader
stops to be a simple file component and imports the hook with a relative path (we are inside a folder component).
./src
└── app
└── blog
└── articles
└── [slug]
└── _components
└── Article
├── Article.tsx
├── ArticleBody.tsx
├── ArticleHeader
│ ├── ArticleHeader.tsx
│ ├── index.ts
│ └── useColoredPart.ts
└── index.ts
It is possible to create a special folder for same-type components. E.g. /hooks
, /validations
, /constants
, etc.
./src
└── app
└── blog
└── articles
└── [slug]
└── _components
└── Article
├── Article.tsx
├── ArticleBody.tsx
├── ArticleHeader
│ ├── ArticleHeader.tsx
│ ├── hooks
│ │ ├── useColoredPart.ts
│ │ └── useSomethingSpecial.ts
│ └── index.ts
└── index.ts
Global components
Article
now needs useColoredPart.ts
and the hook is expected to be used in many, many others. This type of module could be declared as a global one. The same logic has, e.g., UI JSX components. We move them to the root.
./src
├── _components
│ ├── Button.tsx
│ └── FormFieldText.tsx
├── _hooks
│ └── useColoredPart.ts
└── app
└── blog
├── _components
│ ├── ArticlePreview.tsx
│ └── Rating.tsx
├── articles
│ └── [slug]
│ ├── _components
│ │ └── Article
│ │ ├── Article.tsx
│ │ ├── ArticleBody.tsx
│ │ ├── ArticleHeader
│ │ │ ├── ArticleHeader.tsx
│ │ │ ├── index.ts
│ │ │ └── useSomethingSpecial.ts
│ │ └── index.ts
│ └── page.tsx
└── page.tsx
These components theoretically may be used across projects. Absolute imports inside nested folders, relative within the same level.
A similar concept is used, e.g., by the npm package manager, which places installed dependencies into a file system tree. The only difference is that npm considers all dependencies root-first, not leaf-first.
Conclusion
Pros
It is evident which part of an application we can affect after changes in any file.
Rules for imports reduce the number of required import changes across an app in cases of moving or adding complexity to a component.
Cons
- We continually need to check if all rules are fulfilled, especially the rule about the location of the shared component in the nearest common parent folder.