Structure describes and prescribes; it tells about the system while nudging you into thinking of, and using it, in a certain way—hopefully, a way that was chosen deliberately, wisely, and points toward the pit of success.
Discussion of React project structures usually centers on the descriptive (“what does folder/file X contain?”) with an intent of easing navigation (“where do I find object/function Y?”). It’s a start, though one with diminishing returns.
In this note I’d like to offer, rather than my preferred project structure, some ways of thinking about the project space, some ways of slicing it, and a discussion of how they nudge developers into thinking about important properties of the system. Even if those ways and properties aren’t right or sufficient in your case, the process should help you come up with ones that are.
But first: who does your project speak to? Many eyes will visit it over time and you can’t optimize for all. Take a moment to think of which ones matter.
Developer seniority is less of a factor than you’d think: don’t refrain from using words like container or machine in file names just because juniors might find them unfamiliar; if the concepts are central to the architecture, they need to be faced at some point, and it might well be through the filesystem.
Project seniority, on the other hand — the time developers spend on the project — is a factor: a project where people from other teams constantly jump in for just a few days, help with something, and jump out, needs loud signposts to macroscopic concepts that are inevitably forgotten between incursions and thus need constant refreshing; think pages/
. A stabler team can afford to take the basics for granted and use the bandwidth for far finer points; think pure/
(as in “pure functions”).
You step into a supermarket with sixty seconds to grab a microwaveable tandoori chicken. Do you use them to decipher the layout of the aisles, run to the first one that sounds related (“ready dishes”? “world kitchen”?), frantically browse through the shelves, … or do you find a clerk and ask?
I like to think that something like that went through brains at Google’s when faced with:
To eventually lead to:
It’s tempting to slice your project’s space into a multitude of “aisles” and put everything on “its” shelf. We’re just trying to ease navigation! (And, truth be told, we get a kick out of imparting order.)
~/projects/app$
index.jsx
components/
components/forms/
components/pages/
components/layouts/
components/widgets/
Do you look for SubmitButton.jsx
under forms/
or widgets/
? More to the point: do you or anyone on your team actually “browses aisles” when looking for something?
It’s worth asking around, as everyone’s flow differs, but I almost always invoke the project-wide search and type a fragment of filename or actual content; or place cursor on a symbol, and jump to definition. Few keystrokes later, I’m where I need to be.
Browsing is alright when you don’t know what you want and you want to see what’s available. There is some value to organizing for browsing; just not that much, and just not for long. You might still go for it for specific case, such as if people jump in and out of the project often.
The names we pick answer questions:
components/Profile.test.jsx
^ ^ ^
| | +--- "What aspect does it cover?"
| +----------- "What role does it play in the application?"
+---------------------- "What kind of construct is it?"
But we rarely pick our questions consciously. That’s risky because our choice of questions is itself an answer to a more fundamental one: What’s worth focusing on? Focus on the inconsequential and, no matter how good the answers, you’ll move slow, if not backward.
I encourage you to surface your own questions. Here are some that I use and a discussion of each:
If you change a component test, chances are you’ll change the corresponding component soon, and vice-versa. If you delete the component, you don’t want the test lying around. If you look at the file list, you want to spot right away a component that lacks tests.
Strongly related code can, in principle, live in the same file. However, navigating source would require more work, and code would be processed unnecessarily in CI, where seconds matter, and interactive development, where milliseconds do.
Strongly related code shouldn’t live in faraway folders (such as src/
, test/
, stories/
). The small gain in visual tidiness is soon forgotten when an orphaned test has been waiting for months for someone to find the time and check if it can be removed.
Sidecars are a happy mean.
The example below is extreme, but don’t be afraid of going one or two steps further than what you’re used to:
components/
components/Profile.jsx
components/Profile.test.jsx
components/Profile.stories.jsx
components/Profile.Layout.jsx
components/Profile.styles.jsx
components/Profile.reducer.ts
components/Profile.functions.ts
components/Profile.machine.ts
“But won’t this make my components
directory explode?”
Even without sidecars, a medium-sized React application has enough components files to sink way below the fold. At that point, already people aren’t finding things by reading through the file list; two pages or twenty won’t make a difference. But they are looking at subsets of it when selecting files, though, so seeing related things together helps.
[TBD]
When you associate rules to a space, “entering” it is enough to leverage all the thinking that went into creating the rule, without going again through it.
For example, importing stateful (or even just configured) objects in a React component file, like api
below, makes testing hard, because you have to plug into the module import system:
import React, { useState, useEffect } from 'react'
import { Profile } from 'src/components/Profile'
import api from 'src/api'
export const ProfilePage = () => {
const [profile, setProfile] = useState(null)
useEffect(() => {
api.getProfile().then(setProfile)
}, [])
return profile ? <Profile profile={profile} /> : null
}
Thus the rule that a component file can import as many functions as it wants, but state should only “enter” via props or hooks.
That’s workable, but becomes even quicker to handle when you state it as:
“Files under components/
can only import from hooks/
, and components/
itself, and files under hooks/
can only import from hooks/
itself.”
So what happens when I, as a React junior unaware of all that context, am writing components/ProfilePage.jsx
and either my reviewer or my linter tells me I can’t import api
because it’s not under components/
or hooks/
?
It’s not a component, so I try to fit it under hooks/
. Good, but that just shifts the problem: how do I get api
from hooks/
if I’m only allowed to import from hooks/
? Some head-scratching later and questions later, I’m pointed to React Context. I read about it, and I end up creating one, and changing the top-level index.jsx
to:
import api from './api'
ReactDOM.render(
<ApiProvider value={api}>
<App />
</ApiProvider>,
)
The same ApiProvider
can be used for testing without acrobatics, and my reviewer is happy.
By constraining my options, the project structure guided me to a better implementation.
[TBD]
[TBD]