React folder structure recommendations (based on Next.js AppRouter)

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 on Nav).

  • 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.

    • FileComponentName.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.

    • FolderComponentName has a similar interface as a file – exposes 1 React component and secondary variables via ComponentName/index.ts so that it can replace a file ComponentName.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 provides NavItem, 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.
0
Subscribe to my newsletter

Read articles from Sergey Dunaevskiy directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Sergey Dunaevskiy
Sergey Dunaevskiy