pwshub.com

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 since I started using React in 2015, 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 headingInteractive 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 I use this same structure in my TypeScript projects and it works just as well!

Link to this headingMy priorities

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:

import Button from '../Button';

// Or, using Webpack aliases:
// (We'll talk about this further down!)
import Button from '@/components/Button';

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 headingThe implementation

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

Link to this headingComponents

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

src/
└── components/
    └── FileViewer/
        ├── Directory.js
        ├── File.js
        ├── FileContent.js
        ├── FileViewer.helpers.js
        ├── FileViewer.js
        ├── index.js
        └── Sidebar.js

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:

export * from './FileViewer';
export { default } from './FileViewer';

This is essentially a redirection. When we try to import this file, the bundler will be “forwarded” to ./FileViewer.js, and will pull the import from that file instead. 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:

import FileViewer from '../FileViewer/FileViewer';

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

import FileViewer from '../FileViewer';

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.

Link to this headingHooks

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.)

Link to this headingHelpers

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.

Link to this headingUtils

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:

export const sampleOne = (arr) => {
  return arr[Math.floor(Math.random() * arr.length)];
};

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:

export function moveCursorWithinInput(input, position) {
  // All modern browsers support this method, but we don't want to
  // crash on older browsers.
  if (!input.setSelectionRange) {
    return;
  }

  input.focus();
  input.setSelectionRange(position, position);
}

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):

export const getDistanceBetweenPoints = (p1, p2) => {
  const deltaX = Math.abs(p2.x - p1.x);
  const deltaY = Math.abs(p2.y - p1.y);

  return Math.sqrt(deltaX ** 2 + deltaY ** 2);
};

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.

Link to this headingConstants

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.

Link to this headingPages

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.

Link to this headingTradeoffs

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 headingBarrel files

As I mentioned above, this pattern uses “barrel files”. Each main component directory has an index.js that does nothing except re-export other stuff from its sibling files.

In theory, this is a problem because it means the bundler has to do a lot more work, but I don’t believe it’s something that most of us need to worry about.

This blog has about 1200 TS/JS files, and ~15% of them are barrel files. My node_modules directory, by contrast, has ~50k TS/JS files. So really, the bundler will spend most of its time dealing with third-party dependencies. Less than 1% of the modules that the bundler encounters will be barrel files that I’ve created.

If you’re working on an NPM package, it’s probably a good idea to avoid barrel files, especially if it’s a library like lodash with hundreds of individual modules. But I don’t really think it’s a significant issue if you’re building web applications, unless that application has tens of thousands of files and millions of lines of code. At that sort of scale, I can imagine it making a tangible difference.

Link to this headingMore 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(opens in new tab), 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! I’m not actively maintaining it, but you can always fork it to make whatever alterations you wish, and to match your preferred conventions.

Link to this headingIssues with the App Router

As I shared in “How I Built My Blog”, I recently migrated this blog to Next’s new App Router. Unfortunately, when I use my new-component package as-is, I get an error:

**Syntax error:** the name `default` is exported multiple times

This is because my barrel files have the following structure:

export * from './FileViewer';
export { default } from './FileViewer';

Essentially, the bundler is upset because * exports everything, including default. So it’s being exported twice.

When I omit the second import, however, I get a different error during build:

**Type error:** Module "/components/FileViewer/index" has no default export.

Frustrating! The only solution I’ve found is to remove the wildcard import:

export { default } from './FileViewer';

This isn’t as nice, since it means nothing else will be exported. Sometimes I also want to use named exports, and with this approach, I have to remember to add them manually:

export { default, SomethingElse } from './FileViewer';

I haven’t had the bandwidth to look into this; if you’d like to use my new-component(opens in new tab) package with the Next.js App Router, I suggest forking it and removing this line(opens in new tab) from the code.

Link to this headingOrganized 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:

src/
├── base/
│   └── components/
│       ├── Button.js
│       ├── Dropdown.js
│       ├── Heading.js
│       └── Input.js
├── search/
│   ├── components/
│   │   ├── SearchInput.js
│   │   └── SearchResults.js
│   └── search.helpers.js
└── users/
    ├── components/
    │   ├── AuthPage.js
    │   ├── ForgotPasswordForm.js
    │   └── LoginForm.js
    └── use-user.js

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 headingWebpack 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:

// This:
import { sortCategories } from '../../helpers/category.helpers';

// …turns into this:
import { sortCategories } from '@/helpers/category.helpers';

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(opens in new tab).

Link to this headingThe 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.

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

Link to this headingBonus: 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 on

October 2nd, 2024

# of hits

Source: joshwcomeau.com

Related stories
3 weeks ago - An in-depth look at the technical stack behind this very blog! We'll see how I use Next's API routes to implement my hit and like counters, how I use MDX to add interaction and customization, and how I organize my codebase, among others.
1 month ago - An in-depth tutorial that teaches how to create one of the most adorable interactions I've ever created. We'll learn how to use React components and hooks to abstract behaviours, and see how to design the perfect API. Even if you're not...
1 month ago - In our community, it's so common for developer projects to be open-source. I'm breaking with this trend for my blog, but I have good reasons! In this article, I'll share my reasoning, as well as a workaround in case you _really_ want to...
1 month ago - “Should I use pixels or rems?”. In this comprehensive blog post, we'll answer this question once and for all. You'll learn about the accessibility implications, and how to determine the best unit to use in any scenario.
1 month ago - This year's Blue Ridge Ruby conference in Asheville, North Carolina, was not only a wonderful experience but also highlighted the opportunity (and what's at stake) in the Ruby community.
Other stories
26 minutes ago - Cloud hosting is a type of web hosting that hosts websites and applications on virtual servers provisioned across multiple geographic locations. It provides scalability, redundancy, dedicated resources, and superior control compared to...
1 hour ago - Ophir Wainer talks about how starting her career in product as a product leader influences her approach to the craft. The post Leader Spotlight: Jumping straight into product leadership, with Ophir Wainer appeared first on LogRocket Blog.
3 hours ago - AI image generator uses machine learning algorithms and deep learning models to create realistic or artistic images from text prompts or existing visuals. AI image generators can be used in various industries for tasks like creating...
10 hours ago - A 502 Bad Gateway error in Nginx may be a sign of more severe problems, so developers must know how to troubleshoot and resolve these errors.
12 hours ago - Here’s a thorough guide that covers everything you need to know to migrate your CommonJS project to ESM.