Tommy's Dope React (TDR) Project Layout
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 grisled 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
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, and
this often works against projects and teams because 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 layout 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.
- Implementation code should always live in meaningfully named files.
- A particular piece of code should have a short list of good potential locations to live.
- Implementation details should be hidden behind a module interface to make refactoring easier.
- The location of a module or file provides hints on how and where that code is intended to be used.
- The layers of the application and their dependencies between eachother should be apparent by the layout.
- The directory layout should provide meaningful guidance regardless of size: whether 30 files or 3000 files.
- 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. I call it Tommy’s Dope React (TDR) Project Layout, and have used it to great success with my clients:
Here are some callouts to discuss:
- 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/ └── ...
forms/ module is composed individually of
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
lib/ folder is the place to collect your non-UI code. The kind of code and
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.
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/core/, and to let the remaining folders emerge organically.
“pages/” vs. “ui/”
The split between
src/ui/ is also purposeful and by design.
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
lib/ code together. Generally speaking, code should
never depend upon (i.e. import) code from
pages/ except for (a)
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
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
- Code from
pages/should import implementation code from
- Code from
ui/may import code from
lib/but never from
- Generally, code may import from within their own top-level namespace: i.e.
This explanation of the TDR Project Layout is good 80%-rule guidance: 80% of the time it works every time. However, 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.