I fully understand that talking about the "right" way to organize a React project (or, a project using any framework in any language) is a bit like talking about the "right" way to style your hair. (Although I think we can all agree that the objectively "right" way to style your hair is, quite obviously, in a Mohawk.)
As "basic" as project layout can be, I still find myself - after a quarter century in this game - constantly tweaking and evolving my "default" project structure. So before I dive into more tactical details of my Spotify Toolz project (https://www.spotifytoolz.com), I wanted to crank out a quick article about how I currently organize my React projects.
I also welcome some "audience participation" on this article. Even now, after all this time slinging code, it seems that every six-months-or-so, I come to some stunning realization that "this data should really be stored over there!" So I'd love to see your best practices for how to organize projects.
As with all issues as subjective as "project organization", I can 100% guarantee that my current approach is the empirically best approach. I can also guarantee that any other approach is "wrong". And that, six months from now, I will have adopted an entirely different approach to project organization. At that time, I'll scoff at anyone who follows the organization in this article and I'll dismissively tell them that I've moved on to a far superior organization scheme.
If you use this organization scheme, and you eventually grow unsatisfied with it, I will gleefully offer to refund 150% of the money that you paid to read this article.
Basic Organization
(If you can't fathom what the above image represents, I will try to forgive you. Suffice it so say that it's something akin to a phonograph, or a buggy whip.)
Most of my (latest) React projects have a structure pretty close to this:
/src
app.js
/shared
/classes
/components
/css
/functions
/hooks
/objects
/models
/routes
If there is any UI aspect to my app at all, I usually assume that I'll be using React Router. And if I'm using React Router, then the /routes
directory becomes a one-to-one representation of the (faux) directories that the user sees as they navigate through the app.
So, if the app has a users
module (/user
), which then has separate "pages" to create (/user-create
), edit (/user-edit
), and view (/user-view
) a user, my project would look like this:
/src
app.js
/shared
/classes
/components
/css
/functions
/hooks
/objects
/models
/routes
/user
/create
/edit
/view
Furthermore, when I create components that map to these routes, they're represented by JS files under their appropriate folders. So, once we fill in the base components for each route, the tree looks like this:
/src
app.js
/shared
/classes
/components
/css
/functions
/hooks
/objects
/models
/routes
/user
/create
/components
create.user.js
/edit
/components
edit.user.js
/view
/components
view.user.js
Notice that there are no files directly under the /routes/{routeName}
folders. This is because everything that defines a route should logically fall under either the classes
, components
, css
, functions
, hooks
, or objects
folders.
On a practical level, this means that most of the logic for my routes are located under /src/routes/{routeName}/components/{route.name.js}
. Because, for most of my routes, all of the route-specific logic is encapsulated in /src/routes/{routeName}/components/{route.name.js}
.
Now let's imagine that view.user.js
(which will be the <ViewUser>
component) requires a function that is called getLastUserLoginTimestamp()
. When I create that function, I have an organizational choice to make. The choice is determined by this question:
Will this function be used in this component, and only ever in this component??
If this answer is "yes" (i.e., if this function is completely unique and solely targeted to this component), then I would create a structure that looks like this:
/src
app.js
/shared
/classes
/components
/css
/functions
/hooks
/objects
/models
/routes
/user
/create
/components
create.user.js
/edit
/components
edit.user.js
/view
/components
view.user.js
/functions
get.last.user.login.timestamp.js
In this scenario, I've decided that the getLastUserLoginTimestamp()
function will only ever be used in the ViewUser
component. For that reason, I created a separate /functions
directory under the /src/routes/user/view
directory. The implication is that getLastLoginTimestamp()
will only ever be used inside the ViewUser
component. And thus, the /functions
directory that houses the function should only ever live under /src/routes/user/view
.
But to be frank, the above example is rare. Typically, when I'm creating helper functions, I already know they'll be used in other places throughout the app. In fact, even if I'm not sure how they will be used throughout the app, I usually assume that the functions I'm creating will, eventually, be used in other places.
For this reason, I rarely house the functions under a specific /src/routes/{routeName}
directory. More often than not, I house those functions under the /shared
directory. So that would look like this:
/src
app.js
/shared
/classes
/components
/css
/functions
get.last.user.login.timestamp.js
/hooks
/objects
/models
/routes
/user
/create
/components
create.user.js
/edit
/components
edit.user.js
/view
/components
view.user.js
Sharing Is Caring
If it's not clear already, the '/src/shared' directory in my apps houses the lion's share of all my application logic. This happens for two reasons:
Many classes / components / styles / functions / hooks / objects are designed, from the very start, to be "universal". Even if I don't know how a particular file will be re-used in the future, I'm typically writing my files in such a way that I assume they'll be re-used. And thus, most of those files end up being housed under
/src/shared
.Even if it seems like a given class / component / style / function / hook / object will only ever be used in a single route, I tend to save the file under
/src/shared
unless I'm absolutely 100% certain that the file will never possibly be used anywhere else.
This tends to mean that my /src/shared
directory is an ever-growing library of potentially-reusable assets. It also means that my /src/routes
directories are sparse - but they are a fairly simple one-to-one mapping of the user's potential paths through the application.
Important Notes
At this point, I typically write all of my React components as function-based components. This means that I don't use export class SomeComponent extends React.Component {...}
. Instead, I write export const SomeComponent = () => {...}
.
So when you look at the directory structure above and you see /src/shared/classes
, it might be tempting to think that this directory houses class-based components. But that's not the case.
In my chosen project structure, /src/shared/classes
only houses utility helper classes. For example, I frequently use a helper class for localStorage
(that you can read about here: https://dev.to/bytebodger/getting-more-out-of-and-into-storage-with-javascript-41li) and a validation library (that you can read about here: https://dev.to/bytebodger/better-typescript-with-javascript-4ke5). This is my only real use of classes in my most-recent React development.
You'll notice that, under /src/shared
, there's a /components
directory. This isn't for the "main" components that define routes. This is for all of those "helper" components (e.g., Higher Order Components) that I end up using repeatedly throughout my app.
In my particular approach, the /src/shared/css
folder typically houses actual CSS classes. If I'm using inline-CSS within my JSX, that's defined in /src/shared/objects
(because, with inline CSS, styles are JavaScript objects).
I rarely ever create a Hook that does not live under /src/shared/hooks
. In my way of thinking, if your Hook will never be shared between multiple components, then why wouldn't you just define it in the body of the single functional component where it's used??
Finally, my use of /src/objects
may be confusing to some. I've found a number of use-cases for "plain ol JS objects" in my dev. You can find one example of that here: https://dev.to/bytebodger/hacking-react-hooks-shared-global-state-553b and here: https://dev.to/bytebodger/why-is-this-an-anti-pattern-in-react-427p
As for my use of /src/objects/models
, that's explained with my validation library here: https://dev.to/bytebodger/better-typescript-with-javascript-4ke5 In brief, my /src/objects/models
helps me to validate the shape of objects that are being passed into my functions.
Show Me Yours
This is how I currently organize React projects. (Which I'm sure we'll all agree is the right way.) How do you organize your projects? Have I overlooked anything??? Lemme know...
Top comments (3)
I for one also like having specific rules for folder structure. Would add a few things that correspond to how I structure code:
shared
when they are actually sharedSounds like our approaches aren't terribly different. It just seems like I'm more likely to put things in
/shared
by default, and you're more likely to put them in/routes
(or however you name your "feature" directories) by default. Of course, with modern IDEs, it's usually pretty fast-and-simple to drag items into/out-of shared/features in the event that you feel they belong somewhere else in the future.Thanks for the feedback!
Love the assertiveness :)