Delightful React File/Directory Structure

Introduction

React is famously unopinionated when it comes to file/directory structure. How should you structure the files and directories in your applications?

Well, there is no one “right” way, but I've tried lots of different approaches in the 7+ years I've been using React, and I've iterated my way to a solution I'm really happy with.

In this blog post, I'll share the structure I use across all my current projects, including this blog and my custom course platform.

Link to this heading
Interactive file explorer

Alright, so I'll explain everything in depth, but I thought it'd be fun to let you take a self-guided tour first.

Here's an interactive file explorer. Feel free to poke around and see how things are structured!

The files in this demo are JavaScript, but this structure works just as well for TypeScript projects!

Let's start by talking about my priorities, the things I've optimized for.

First, I want to make it easy to import components. I want to be able to write this:

js

Next: when I'm working in my IDE, I don't want to be flooded with index.js files. I've worked in projects where the top bar looked like this:

A bunch of files open, all called “index.js”

To be fair, most editors will now include the parent directory when multiple index.js files are open at once, but then each tab takes up way more space:

A bunch of files open, formatted like “index.js - /RainbowButton”
A bunch of files open, formatted like “index.js - /RainbowButton”

My goal is to have nice, clean component file names, like this:

A bunch of files open, with proper names like “RainbowButton.js”
A bunch of files open, with proper names like “RainbowButton.js”

Finally, in terms of organization, I want things to be organized by function, not by feature. I want a “components” directory, a “hooks” directory, a “helpers” directory, and so on.

Sometimes, a complex component will have a bunch of associated files. These include:

  • "Sub-components", smaller components used exclusively by the main component
  • Helper functions
  • Custom hooks
  • Constants or data shared between the component and its associated files

As a real example, let's talk about the FileViewer component, used in this blog post for the “interactive file explorer” demo. Here are the files created specifically for this component:

  • FileViewer.js — the main component
  • FileContent.js — the component that renders the contents of a file, with syntax highlighting
  • Sidebar.js — The list of files and directories that can be explored
  • Directory.js — the collapsible directory, to be used in the sidebar
  • File.js — An individual file, to be used in the sidebar
  • FileViewer.helpers.js — helper functions to traverse the tree and help manage the expanding/collapsing functionality

Ideally, all of these files should be tucked away, out of sight. They're only needed when I'm working on the FileViewer component, and so I should only see them when I'm working on FileViewer.

Link to this heading
The implementation

Alright, so let's talk about how my implementation addresses these priorities.

Here's an example component, with all the files and directories required to accomplish my goals:

Most of these files are the ones mentioned earlier, the files needed for the FileViewer component. The exception is index.js. That's new.

If we open it up, we see something a bit curious:

js

This is essentially a redirection. When we try to import this file, we'll be forwarded to the FileViewer.js file in the same directory. FileViewer.js holds the actual code for this component.

Why not keep the code in index.js directly? Well, then our editor will fill up with index.js files! I don't want that.

Why have this file at all? It simplifies imports. Otherwise, we'd have to drill into the component directory and select the file manually, like this:

js

With our index.js forwarding, we can shorten it to just:

js

Why does this work? Well, FileViewer is a directory, and when we try to import a directory, the bundler will seek out an index file (index.js, index.ts, etc). This is a convention carried over from web servers: my-website.com will automatically try to load index.html, so that the user doesn't have to write my-website.com/index.html.

In fact, I think it helps to think of this in terms of an HTTP request. When we import src/components/FileViewer, the bundler will see that we're importing a directory and automatically load index.js. The index.js does a metaphorical 301 REDIRECT to src/components/FileViewer/FileViewer.js.

It may seem over-engineered, but this structure ticks all of my boxes, and I love it.

If a hook is specific to a component, I'll keep it alongside that component. But what if the hook is generic, and meant to be used by lots of components?

In this blog, I have about 50 generalized, reusable hooks. They're collected in the src/hooks directory. Here are some examples:

(This code is real! it's provided here as an example, but feel free to copy the hooks you're interested in.)

What if I have a function that will help me accomplish some goal for the project, not directly tied to a specific component?

For example: this blog has multiple blog post categories, like React, CSS, and Animations. I have some functions that help me sort the categories by the number of posts, or get the formatted / "pretty" name for them. All that stuff lives in a category.helpers.js file, inside src/helpers.

Sometimes, a function will start in a component-specific file (eg. FileViewer/FileViewer.helpers.js), but I'll realize that I need it in multiple spots. It'll get moved over to src/helpers.

Alright, so this one requires some explanation.

A lot of devs treat "helpers" and "utils" as synonyms, but I make a distinction between them.

A helper is something specific to a given project. It wouldn't generally make sense to share helpers between projects; the category.helpers.js functions really only make sense for this blog.

A utility is a generic function that accomplishes an abstract task. Pretty much every function in the lodash library is a utility, according to my definition.

For example, here's a utility I use a lot. It plucks a random item from an array:

js

I have a utils.js file full of these sorts of utility functions.

Why not use an established utility library, like lodash? Sometimes I do, if it's not something I can easily build myself. But no utility library will have all of the utilities I need.

For example, this one moves the user's cursor to a specific point within a text input:

js

And this utility gets the distance between two points on a cartesian plane (something that comes up surprisingly often in projects with non-trivial animations):

js

These utilities live in src/utils.js, and they come with me from project to project. I copy/paste the file when I create a new project. I could publish it through NPM to ensure consistency between projects, but that would add a significant amount of friction, and it's not a trade-off that has been worth it to me. Maybe at some point, but not yet.

Finally, I also have a constants.js file. This file holds app-wide constants. Most of them are style-related (eg. colors, font sizes, breakpoints), but I also store public keys and other “app data” here.

One thing not shown here is the idea of “pages”.

I've omitted this section because it depends what tools you use. When I use something like create-react-app, I don't have pages, and everything is components. But when I use Next.js, I do have /src/pages, with top-level components that define the rough structure for each route.

Every strategy has trade-offs. let's talk about some of the downsides to the file structure approach outlined in this blog post.

Link to this heading
More boilerplate

Whenever I want to create a new component, I need to generate:

  • A new directory, Widget/
  • A new file, Widget/Widget.js
  • The index forwarder, Widget/index.js

That's a lot of work to do upfront!

Fortunately, I don't have to do any of that manually. I created an NPM package, new-component, which does all of this for me automatically.

In my terminal, I type:

When I execute this command, all of the boilerplate is created for me, including the basic component structure I'd otherwise have to type out! It's an incredible time-saver, and in my opinion, it totally nullifies this drawback.

You're welcome to use my package if you'd like! You might want to fork it, to match your preferred conventions.

Link to this heading
Organized by function

In general, there are two broad ways to organize things:

  • By function (components, hooks, helpers…)
  • By feature (search, users, admin…)

Here's an example of how to structure code by feature:

There are things I really like about this. It makes it possible to separate low-level reusable “component library” type components from high-level template-style views and pages. And it makes it easier to quickly get a sense of how the app is structured.

But here's the problem: real life isn't nicely segmented like this, and categorization is actually really hard.

I've worked with a few projects that took this sort of structure, and every time, there have been a few significant sources of friction.

Every time you create a component, you have to decide where that component belongs. If we create a component to search for a specific user, is that part of the “search” concern, or the “users” concern?

Often, the boundaries are blurry, and different developers will make different decisions around what should go where.

When I start work on a new feature, I have to find the files, and they might not be where I expect them to be. Every developer on the project will have their own conceptual model for what should go where, and I'll need to spend time acclimating to their view.

And then there's the really big issue: refactoring.

Products are always evolving and changing, and the boundaries we draw around features today might not make sense tomorrow. When the product changes, it will require a ton of work to move and rename all the files, to recategorize everything so that it's in harmony with the next version of the product.

Realistically, that work won't actually get done. It's too much trouble; the team is already working on stuff, and they have a bunch of half-finished PRs, where they're all editing files that will no longer exist if we move all the files around. It's possible to manage these conflicts, but it's a big pain.

And so, the distance between product features and the code features will drift further and further apart. Eventually, the features in the codebase will be conceptually organized around a product that no longer exists, and so everyone will just have to memorize where everything goes. Instead of being intuitive, the boundaries become totally arbitrary at best, and misleading at worst.

To be fair, it is possible to avoid this worst-case scenario, but it's a lot of extra work for relatively little benefit, in my opinion.

But isn't the alternative too chaotic? It's not uncommon for large projects to have thousands of React components. If you follow my function-based approach, it means you'll have an enormous set of unorganized components sitting side-by-side in src/components.

This might sound like a big deal, but honestly, I feel like it's a small price to pay. At least you know exactly where to look! You don't have to hunt around through dozens of features to find the file you're after. And it takes 0 seconds to figure out where to place each new file you create.

Link to this heading
Webpack aliases

Webpack is the bundler used to package up our code before deployment. There are other bundlers, but most common tools (eg. create-react-app, Next.js, Gatsby) will use Webpack internally.

A popular Webpack feature lets us create aliases, global names that point to a specific file or directory. For example:

js

Here's how it works: I create an alias called @helpers which will point to the /src/helpers directory. Whenever the bundler sees @helpers, it replaces that string with a relative path for that directory.

The main benefit is that it turns a relative path (../../helpers) into an absolute path (@helpers). I never have to think about how many levels of ../ are needed. And when I move files, I don't have to fix/update any import paths.

Implementing Webpack aliases is beyond the scope of this blog post, and will vary depending on the meta-framework used, but you can learn more in the Webpack documentation.

Link to this heading
The Joy of React

So, that's how I structure my React applications!

As I mentioned right at the top, there's no right/wrong way to manage file structure. Every approach prioritizes different things, makes different tradeoffs.

Personally, though, I've found that this structure stays out of my way. I'm able to spend my time doing what I like: building quality user interfaces.

React is so much fun. I've been using it since 2015, and I still feel excited when I get to work with React.

For a few years, I taught at a local coding bootcamp. I've worked one-on-one with tons of developers, answering their questions and helping them get unstuck. I wound up developing the curriculum that this school uses, for all of its instructors.

I want to share the joy of React with more people, and so for the past couple of years, I've been working on something new. An online course that will teach you how to build complex, rich, whimsical, accessible applications with React. The course I wish I had when I was learning React.

Visit the “Joy of React” homepage

You can learn more about the course, and discover the joy of building with React:

Link to this heading
Bonus: Exploring the FileViewer component

Are you curious how I built that FileViewer component up there?

I'll be honest, it's not my best work. But I did hit some interesting challenges, trying to render a recursive structure with React!

If you're curious how it works, you can use the FileViewer component to explore the FileViewer source code. Not all of the context is provided, but it should give you a pretty good idea about how it works!

Last Updated

February 23rd, 2024

Hits