Optimal file structure for React applications

Image for post
Image for post

Dan Abramov famously officialized the file structure for React applications as “move files around until they feel right.” I do not want to disagree with this point. I agree wholeheartedly. However, despite Dan’s advice, the question of optimal file structure still gains traction frequently. Despite absolute freedom, developers are still uncomfortable with exploring new territories; and I think they have a point. It’s a lot of work to refactor a code base for a file structure change, and it takes a lot of trial and error to find one you like. It would be beneficial to know some ground rules before mapping out your expedition — what have those who came before you discovered?

Over the past three years, I’ve gained extensive experience using React from personal projects to corporate settings; from teams of 2 to teams of 16; from React 0.12 to React 16.10; from Redux to ReactN; from the browser to React Native. No matter the context of the React application, the structure “feels right” under all of these conditions.

This article is an opinion piece for what file structure has worked best for me and my teams after our own trial and error. You are more than welcome to adjust it for your own use case. I am interested in maintaining this article as a living document of what has worked best for the React community, under what conditions, and why. Please share your file structure discoveries in the comments below. 💬

To see this file structure in a living application, I have also created an accompanying GitHub repository.

Create React App 👷🏾‍♂️

Image for post
Image for post

I am a huge fan of the create-react-app command. It requires hardly any changes out of the box, and its popularity makes for a foundation recognizable to many developers.

This is the file structure created by create-react-app as of 2.1.5:

This is a solid start.

  • build is the location of your final, production-ready build. This directory won’t exist until you run npm build or yarn build. The contents of this folder should be ready-to-ship without any interaction on your part.
  • node_modules is where packages installed by NPM or Yarn will reside.
  • public is where your static files reside. If the file is not imported by your JavaScript application and must maintain its file name, put it here. Files in the public directory will maintain the same file name in production, which typically means that they will be cached by your client and never downloaded again. If your file does not have a filename that matters — such as index.html, manifest.json, or robots.txt — you should put it in src instead.
  • src is where your dynamic files reside. If the file is imported by your JavaScript application or changes contents, put it here. In order to make sure the client downloads the most up-to-date version of your file instead of relying on a cached copy, Webpack will give changed files a unique file name in the production build. This allows you to use simple, intuitive file names during development, such as banner.png instead of banner-2019-03-01-final.png. You never have to worry about your client using the outdated cached copy, because Webpack will automatically rename banner.png to banner.unique-hash.png, where the unique hash changes only when banner.png changes.

From here, create-react-app gives us total freedom. I’ll walk through the file structure that has worked the most cooperatively with the projects on which I’ve worked.

Tweaking Create React App 🔧

Image for post
Image for post

The immediate two issues I find with create-react-app's output are:

One of the most important and agreed upon structures for a React project is to have a components directory for storing your components. Some developers use two directories — one for stateful components and one for stateless components. As an opinionated article, I’ll discuss what I’ve found most intuitive: a single directory for components. Keep related code as close as possible. That is where we’ll move App.js and its siblings.

With this new structure, the files directly under src are the entry files. This is where Webpack starts. What is your project? It has a base style sheet, it includes a service worker, and it renders a React application to the DOM.

Your React components can now be found in the components directory. We’ll discuss the src/components directory in more depth later.

You may have noticed I added a src/components/index.js file. This file serves as a “barrel” through which all sibling components are exported. export { default as App } from './app';. This index.js file will be a crucial part of the testing experience.

Your assets, which are dependencies shared by your application — such as SASS mixins, images, etc. — can go in their respective directories. This provides a single location for updating dependencies used app-wide. If your branding needs to change, the colors can all be adjusted in the centralized styles directory, your banners can all be adjusted in the centralized images directory. If you need a new build for each localization supported, all the language files can be stored in the centralized locales directory.

Depending on your team structure, it can be a big convenience to group these related, non-JavaScript files together in your otherwise JavaScript project. As a JavaScript developer on a JavaScript project, the management of these non-JavaScript resources may be handled by others — graphic designers, translators, accessibility consultants, or public relations representatives. Keeping the files close allows them to easily be dropped in and out and ensures that no file is left behind in a process to override a dated dependency, such as the migration from one colored theme to another or the localization from original English to newly-supported Spanish. Any asset directory that will be imported via JavaScript should have its own index.js file, as shown with the src/components directory.

The project needs another important addition: a utilities directory, such as src/utils or src/helpers. This is a folder full of helper functions that are used globally. Keep your code DRY (Don’t Repeat Yourself) by exporting repeated logic to a singular location and importing it where used. Parts of your application can now share logic without copy-pasting by placing shared logic in this utilities directory. This directory can go by any name, and I’m not picky about it. I don’t think it makes sense to say that one name is better than the others, but it is a directory that needs to exist. I’ve seen many different names used in enterprise settings; what matters is that your team agrees that the name makes sense for your project. Since the utilities folder will be imported via JavaScript, it should contain an index.js file that re-exports its siblings. The testing implications of this index.js file are discussed in Writing testable React components with hooks.

Finally, the project needs a directory that represents standalone modules that can hypothetically be exported from the project entirely. These modules do not contain business logic related to the application itself. Think open-source code that isn’t open-source yet. Ask yourself, “Can another team benefit from this code?” Like utilities, you may call this directory whatever makes sense for you and your team, such as src/open-source, src/packages, src/shared, or src/standalone. Example modules include home-rolled translation systems, useful implementations of complex algorithms and data structures, or highly reusable components that may offer valuable functionality to other teams. For CloudWatch Logs, our team extended Airbnb’s react-dates component in order to add a time selection in addition to its built-in date selection. This new component was standalone and offered value to other teams. While we developed it in the standalone modules directory, we were able to strip it from the project entirely with minimal effort once we had fully extended, tested, and vetted it. It is now used by other AWS teams, and we receive the benefits of those teams’ feature contributions and bug fixes. Unlike src/components and src/utils, we do not give this directory an index.js file, because we want to fully mimic imports as if it were an external package (import Component from 'open-source-package-name'; and import Component from 'src/open-source/package-name';).

Our repository now looks something like this:

The Component Directory 🍰

Image for post
Image for post

The structure of the Component directory is likely the most important one and the reason someone would look up how to structure a React application, an application made of a collection of Components. We’ve established that Components reside in src/components/component-name, but then what?

A sensible decision is to name the component’s file index.js. This allows you to import it via src/components/component-name, despite that being a directory. When importing a directory, the index.js file is imported. An easily overlooked problem with this approach is that, when editing multiple Components at once in your editor, the tabs are all labeled index.js. This is often useless and impossible to work with.

Image for post
Image for post
index.js / index.js / index.js / index.js / index.js / dropdown.js

Visual Studio Code is aware of this problem and will display directory names alongside file names that are the same. Other editors won’t, and you shouldn’t punish other developers on your project for not using the same editor you do. At the same time, the mass repetition of index.js in your tab navigation is wasted space. We can fix this problem and another at the same time. Let’s look closer.

A component typically involves more than one file. If you are not yet using React hooks, you have the stateless (“dumb”) component, the stateful container, and perhaps a Redux-connecting higher-order component among other higher-order components. Almost all components have SASS files or JSS stylesheets, child components, and even the component’s very own utilities that aren’t shared by other components. This is precisely why the src/components directory is full of component-name directories and not single component-name.js files.

You likely only have one root component in your src/components/component-name directory. You can use hooks to manage that component’s local state, global state, and values pulled from React contexts. You typically do not need higher-order components, and if you do, they wrap neatly around the default export:

To solve the issue of readability in the editor, we put the component itself in component-name.js and have index.js simply export { default } from './component-name.js. For testing purposes, I recommend putting the entire hook state for that component into a single useComponentName hook. You can find more detail about testing hooks in Writing testable React components with hooks.

Your end result looks something like this:

Just like src/components and src/utils, your hooks directory also gets an index.js file for simpler testability.

To summarize, we end up with the above structure.

  • hooks/index.js is an entry file that merely re-exports its siblings.
  • hooks/use-component-name.js is a single hook that calls all other hooks used by the component.
  • component-name.css is a straight-forward CSS file imported by your stateless view component.
  • component-name.scss is a straight-forward SASS file imported by your stateless view component.
  • component-name-styles.js is your JSS. I’ve used this file extensively for storing Material UI withStyles higher-order-components and JSS.
  • index.js is your entry point for importing your component. It contains nothing but export { default as ComponentName } from './component-name'; and any TypeScript types needed to mount your component.

During development, a component tends to change hierarchy rapidly. Does it manage its own state, connect to the global store, or does a parent component pass it a state as props? Does it do just one, any two, or all three of these things? As a separation of concerns and with the readability of file splitting, each of these things should be their own component and own file. But we only have one component-name/index.js, so which gets to be there?

The problem my teams have run into with this approach has been refactoring. During development, that global state may leave the component altogether. It may be handled by the parent component instead and passed as props. It may move to local state. It may not exist at all originally. There is just the view component as the entry point located at component-name/index.js. Then, once the global state has to be added, it forces us to move the entire file from index.js to component-name-view.js and replace the entry point with the global state connector. It’s so much work to move these files around frequently, and it makes for hideous pull requests and file diffs.

After “moving things around until they felt right,” the solution we discovered is that index.js should do one thing: export { default } from './component-name-topmost.js';. You put your stateless component in component-name-view.js, your container component in component-name-container.js, your global state component in component-name-redux.js and simply export whichever happens to be the topmost Component at any point in time. You often start with just a stateless component, so you are exporting view.js. Suddenly, scope demands state, so now you have container.js. You just change view to container in the export statement, and you’re done. Suddenly, scope demands global state, so now you have redux.js. You can just change container to redux in the export statement, and you’re done.

To summarize, we end up with the above structure.

  • component-name-container.js is your business logic and state management as handled before being sent to the stateless view component.
  • component-name-redux.js is the mapStateToProps, mapDispatchToProps, and connect functionality of Redux. If you use an alternative global state management tool, give it a similar file name, such as component-name-mobx.js. This allows you to harness multiple global states (if necessary, though not recommended) and allows you to easily swap global state managers in the future.
  • component-name-view.js is your stateless view component. For the majority of cases, this component should be able to be pure functional component.
  • index.js is your entry point for importing your component. It contains nothing but an export statement that points to the topmost component at any point in time, because the topmost component changes often during development.

Your component-name directory can have its own typed subdirectories such as utils as well. This allows you to code split. Even if you aren’t using those helper functions or constant definitions in multiple locations, stripping giant chunks of code out of your components and replacing them with descriptive names is an extremely powerful tool for your development process and allows you to unit test these functions in a vacuum.

If a component, by definition, is tightly coupled as a child component of another, I nest it directly in its parent component’s directory. There is no benefit to cluttering the higher component directory with a component that is not reusable. It would just be harder to navigate between child and sibling and leave dead code when deprecating a component.

My portfolio’s web application has a section for GitHub repositories. Each item on the list is a component made up of two components — the icon and the text. All of these components are dedicated to being a GitHub repository list for a portfolio. They are not reusable elsewhere on my website, even though they may be made up of reusable components. My file structure looks like so:

Subdirectories are capped at a depth of 1. Too many deeply-nested directories becomes hard to code review, follow along, and find the component you are seeking when editing.

If my GitHuboRepo component uses an Icon component which uses an Image component which uses an Svg component (and we’re going on the assumption that Icon, Image, and Svg are not reusable for the sake of example), it would be structured like so:

Even though Image and Svg are only ever imported by Icon, they live as siblings, because we’ve already reached the maximum nested depth.

In short, your components directory should never be deeper than:

This applies equally to non-component directory types such as src/components/component-name/utils/utility-name.

React Router 🔗

Image for post
Image for post

An additional directory I like to add is src/routes. These are the Components provided directly to react-router's <Route>s. If I have a page located at charlesstover.com/portfolio, I will have that Component entry point be at src/routes/portfolio/index.js. It may use Components from src/components, but route entry points are unique in that their best name is where they are not what they do. Because of this, they get the special treatment of the routes directory.

Tests 📄

Lastly, unit tests. Continuing the principle of keeping related files together, you should store them alongside your tested files. This allows you to easily navigate from your Component to its test file, import the tested files, and keep tests in sync with the files they are testing.

I’ve mentioned three times the importance of using index.js as an entry point for “type directories” such as src/components, src/utils, or src/components/component-name/hooks. The testing advantages of this index.js file are discussed in Writing testable React components with hooks.

Conclusion 🔚

If you have any questions or great file structure input, please leave them in the comments below.

To read more of my columns, you may follow me on LinkedIn and Twitter, or check out my portfolio on CharlesStover.com.

Written by

Senior Full Stack JavaScript Developer / charlesstover.com

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store