Skip to main content
Test Double company logo
Services
Services Overview
Holistic software investment consulting
Software Delivery
Accelerate quality software development
Product Management
Launch modern product orgs
Legacy Modernization
Renovate legacy software systems
DevOps
Scale infrastructure smoothly
Upgrade Rails
Update Rails versions seamlessly
Technical Recruitment
Build tech & product teams
Technical Assessments
Uncover root causes & improvements
Case Studies
Solutions
Accelerate Quality Software
Software Delivery, DevOps, & Product Delivery
Maximize Software Investments
Product Performance, Product Scaling, & Technical Assessments
Future-Proof Innovative Software
Legacy Modernization, Product Transformation, Upgrade Rails, Technical Recruitment
About
About
What's a test double?
Approach
Meeting you where you are
Founder's Story
The origin of our mission
Culture
Culture & Careers
Double Agents decoded
Great Causes
Great code for great causes
EDI
Equity, diversity & inclusion
Insights
All Insights
Hot takes and tips for all things software
Leadership
Bold opinions and insights for tech leaders
Developer
Essential coding tutorials and tools
Product Manager
Practical advice for real-world challenges
Say Hello
Test Double logo
Menu
Services
BackGrid of dots icon
Services Overview
Holistic software investment consulting
Software Delivery
Accelerate quality software development
Product Management
Launch modern product orgs
Legacy Modernization
Renovate legacy software systems
Cycle icon
DevOps
Scale infrastructure smoothly
Upgrade Rails
Update Rails versions seamlessly
Technical Recruitment
Build tech & product teams
Technical Assessments
Uncover root causes & improvements
Case Studies
Solutions
Solutions
Accelerate Quality Software
Software Delivery, DevOps, & Product Delivery
Maximize Software Investments
Product Performance, Product Scaling, & Technical Assessments
Future-Proof Innovative Software
Legacy Modernization, Product Transformation, Upgrade Rails, Technical Recruitment
About
About
About
What's a test double?
Approach
Meeting you where you are
Founder's Story
The origin of our mission
Culture
Culture
Culture & Careers
Double Agents decoded
Great Causes
Great code for great causes
EDI
Equity, diversity & inclusion
Insights
Insights
All Insights
Hot takes and tips for all things software
Leadership
Bold opinions and insights for tech leaders
Developer
Essential coding tutorials and tools
Product Manager
Practical advice for real-world challenges
Say hello
Developers
Developers
Developers
Legacy rescue

How to optimize your React project structure for better results

Organize your React project like a pro! Learn tips for better refactoring, hiding implementation details, and scalable growth. Start improving your codebase today.
Tommy Groshong
|
November 29, 2021
Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.

A few upsides come with adopting an opinionated framework like Ruby on Rails. One of them is having a clear pattern to the layout of a project’s source code directories and guidance on where specific code should live.

This reduces the friction of creating new code modules and provides guideposts for navigating the code base. Human brains like patterns, and organizing your code into clear patterns helps developers find their way; both the newly onboarding devs and the grizzled veteran devs.

Opinionated direction is something we severely lack in many aspects of React projects, and directory layout is definitely one. Tools like Create React App have done a great job at scaffolding a working React project with a few top-level files, configs, and folders with a functional build pipeline, linting toolchain, and test harness already configured. But CRA gives you a src/ directory for all your application code with no guidance on how the files inside should be organized.

Once you’re inside a src/ folder of a React project, it’s the wild west. This often works against projects and teams. Unless they have already done a few React projects and cut themselves on all the sharp corners, it’s easy to stumble into traps and pitfalls that actively work against good React behaviors. Refactoring code becomes scary, exposed implementation details cause unnecessary tight coupling, and the pain gets worse as your project grows.

But all is not lost!

How to lay out your React project src/ folder

“You have brains in your head. You have feet in your shoes. You can steer yourself any direction you choose. You’re on your own. And you know what you know. And YOU are the one who’ll decide where to go…”

― Dr. Seuss, Oh, the Places You’ll Go!

Let’s lay some foundational principles that are conducive to refactoring, hide implementation details, and grow with your project.

  1. Implementation code should always live in meaningfully named files.
  2. A particular piece of code should have a short list of good potential locations to live.
  3. Implementation details should be hidden behind a module interface to make refactoring easier.
  4. The location of a module or file provides hints on how and where that code is intended to be used.
  5. The layers of the application and their dependencies between each other should be apparent by the layout.
  6. The directory layout should provide meaningful guidance regardless of size: whether 30 files or 3000 files.
  7. The directory layout should reward you for good React behaviors: e.g. extracting many small components to meaningful places, extracting non-UI code out of React all together.

With those principles in hand, let’s look at an example of a project layout following these principles. This super useful and flexible React project layout has been used with great success at many clients:

.
├── build/                               #
├── docs/                                #
├── src/                                 #
│   ├── lib/                             # 'lib/' is Non-UI Code
│   │   ├── __tests__/                   # Tests in `__tests__/` dirs
│   │   │   └──  ...                     #
│   │   ├── core/                        # 'core/' is bedrock domain code
│   │   │   ├── __tests__/               #
│   │   │   │   ├──  invoices.test.js    #
│   │   │   │   ├──  tasks.test.js       #
│   │   │   │   └──  ...                 #
│   │   │   ├── index.js                 #
│   │   │   ├── invoices.js              # Pure functions of business logic
│   │   │   ├── tasks.js                 # for 'invoices' domain 'tasks' domain.
│   │   │   └── (otherDomains).js        #
│   │   ├── ...                          # Other infrastructural code that isn't
│   │   ├── httpClient.js                # tied to our domain.
│   │   ├── moneyFormatter.js            #
│   │   ├── dateFormatter.js             #
│   │   ├── websocketHandler.js          #
│   │   └── ...                          #
│   ├── pages/                           # Entrypoint components for routes.
│   │   ├── auth/                        # Each dir could map to a URL prefix.
│   │   │   ├── index.js                 # (If Next.js, follow Next.js rules)
│   │   │   ├── Login.js                 #
│   │   │   ├── ForgotPassword.js        #
│   │   │   └── Logout.js                #
│   │   ├── invoices/                    #
│   │   │   ├── index.js                 #
│   │   │   ├── InvoiceDetails.js        #
│   │   │   └── InvoiceList.js           #
│   │   └── dashboard/                   #
│   │       └── ...                      #
│   ├── ui/                              # React components, contexts, and hooks
│   │   ├── login/                       # grouped by domain or category.
│   │   │   ├── __tests__/               #
│   │   │   │   ├──  LoginForm.test.js   #
│   │   │   │   └──  ...                 #
│   │   │   ├── index.js                 # <- Only public exports for this module
│   │   │   ├── LoginForm.js             # <- Login related Component
│   │   │   ├── useAuthStatus.js         # <- Login related hook
│   │   │   └── AuthProvider.js          # <- Login related context provider
│   │   ├── forms/                       #
│   │   │   ├── __tests__/               #
│   │   │   ├── index.js                 #
│   │   │   ├── TextInput.js             # <- A lone component
│   │   │   ├── button/                  #
│   │   │   │   ├── index.js             #
│   │   │   │   ├── some_helper.js       #
│   │   │   │   ├── InnerButton.js       # <- Private, non-exported component
│   │   │   │   └── FancyButton.js       # <- Primary implementation of component
│   │   │   ├── checkbox/                #
│   │   │   │   └── ...                  #
│   │   │   └── select/                  #
│   │   │       └── ...                  #
│   │   └── avatars/                     #
│   │       └── ...                      #
│   ├── ...                              #
│   ├── App.js                           # <- Compose your top-level routes.
│   └── index.js                         # <- (a) render `App` with global providers
├── LICENSE                              #    (b) mount React tree to the DOM.
└── README.md

Here are some callouts to discuss:

  1. Nesting modules
  2. lib/
  3. pages/ vs. ui/
  4. Dependencies

Nesting modules

Notice this layout is more deep than wide, more nested than flat. This is by design.

By nesting modules the developer can indicate dependency relationships between code. For instance, components that are only meant to be consumed by a specific higher-level component are nested within that higher-level component’s folder. For example:

└── src/
    └── ui/
        └── forms/
            ├── button/
            │   ├── index.js
            │   └── Button.js
            ├── checkbox/
            │   ├── index.js               # Only exports 'Checkbox' component
            │   ├── SimpleCheckbox.js      # Imports 'CheckSelectOverlay'
            │   ├── FancyCheckbox.js       # Imports 'CheckSelectOverlay'
            │   └── CheckSelectOverlay.js  # Only relevant to checkbox components
            └── select/
                └── ...

The forms/ module is composed individually of button/, checkbox/, select/, and potentially many more “forms” related modules exporting their own components. Those sub-modules can judiciously decide which code from which modules to expose via the index.js file, but they could also have deeping nesting inside of them if it makes sense.

The “lib/” folder

The lib/ folder is the place to collect your non-UI code. The kind of code and operations that could/should survive completely changing your JavaScript framework. The lib/core/ folder is a personal favorite, as that is the place to collect your domain objects and business logic, usually in the form of pure functions. The wider lib/ folder should contain infrastructural code like network layer clients, formatting utilities, platform communication like Browser Storage or IndexDB, etc.

It is appropriate to further subdivide lib/ into useful modules as it grows. For instance:

src/
└── lib/
    ├── core/
    │   ├── index.js
    │   ├── invoices.js
    │   └── tasks.js
    ├── network/
    │   ├── index.js
    │   ├── httpClient.js
    │   └── websocketHandler.js
    ├── storage/
    │   ├── index.js
    │   ├── webIndexDB.js
    │   └── localStorage.js
    ├── formatters/
    │   ├── index.js
    │   ├── dateFormatter.js
    │   └── moneyFormatter.js
    ├── testUtils/
    │   ├── index.js
    │   └── dataFactories.js
    └── ...

At the start of a project, I recommend beginning with only lib/ and lib/core/, and to let the remaining folders emerge organically.

“pages/” vs. “ui/”

The split between src/pages/ and src/ui/ is also purposeful and by design. While ui/ is meant to be a collection of reusable React code(i.e. components, hooks, and contexts ), pages/ specifically (a) maps components to routes and (b) composes ui/ and lib/ code together. Generally speaking, code should never depend upon (i.e. import) code from pages/ except for (a) src/App.js setting up the top-level routes and (b) parent pages on sub-pages (i.e. component for route http://example.org/invoices imports component page handing route http://example.org/invoices/:id.).

Dependencies: Made more explicit

By knowing the location of code, you should have an idea on what code is most likely to depend, or not depend, on it. In this layout, the following rules are expected to hold:

  • Code from lib/ never imports code from ui/ or pages/.
  • Code from pages/ should import implementation code from ui/ or lib/.
  • Code from ui/ may import code from lib/ but never from pages/.
  • Generally, code may import from within their own top-level namespace: i.e. pages/, lib/, ui/.

Conclusion

This React project layout prioritizes patterns that are conducive to refactoring, hide implementation details, and grow with your project.

This pattern works for (a) Create React App projects, (b) Next.js projects, (c) React Native projects, and … even non-React projects. GASP! As more and more frameworks get on the “component” bandwagon, they hit the same code organization growing pains. This layout can be easily modified to work for Vue, Angular, and vanilla Web Components as well. It even supports very nicely JavaScript alternatives like TypeScript and Flow: it’s easy to put shared interfaces in meaningful locations and the dependency rules still work.

This explanation of the super useful and flexible React Project Layout is good 80%-rule guidance: 80% of the time it works every time. It doesn’t clearly explain more niche or esoteric cases, but hopefully it gives you enough of a starting point that you can make those decisions with confidence.

So give it a shot, and let me know what you think. Reach me at @tgroshon on Twitter.

Related Insights

🔗
The nine best recommendations in the new React docs
🔗
Effective React testing
🔗
Debugging React routing
🔗
Model View Controller pattern in React: A deep dive

Explore our insights

See all insights
Developers
Developers
Developers
You’re holding it wrong! The double loop model for agentic coding

Joé Dupuis has noticed an influx of videos and blog posts about the "correct" way of working with AI agents. Joé thinks most of it is bad advice, and has a better approach he wants to show you.

by
Joé Dupuis
Leadership
Leadership
Leadership
Don't play it safe: Improve your continuous discovery process to reduce risk

We often front-load discovery to feel confident before building—but that’s not real agility. This post explores how continuous learning reduces risk better than perfect plans ever could.

by
Doc Norton
Leadership
Leadership
Leadership
How an early-stage startup engineering team improved the bottom line fast

A fast-growing startup was burning cash faster than it could scale. Here’s how smart engineering decisions helped them improve the bottom line.

by
Jonathon Baugh
Letter art spelling out NEAT

Join the conversation

Technology is a means to an end: answers to very human questions. That’s why we created a community for developers and product managers.

Explore the community
Test Double Executive Leadership Team

Learn about our team

Like what we have to say about building great software and great teams?

Get to know us
Test Double company logo
Improving the way the world builds software.
What we do
Services OverviewSoftware DeliveryProduct ManagementLegacy ModernizationDevOpsUpgrade RailsTechnical RecruitmentTechnical Assessments
Who WE ARE
About UsCulture & CareersGreat CausesEDIOur TeamContact UsNews & AwardsN.E.A.T.
Resources
Case StudiesAll InsightsLeadership InsightsDeveloper InsightsProduct InsightsPairing & Office Hours
NEWSLETTER
Sign up hear about our latest innovations.
Your email has been added!
Oops! Something went wrong while submitting the form.
Standard Ruby badge
614.349.4279hello@testdouble.com
Privacy Policy
© 2020 Test Double. All Rights Reserved.