Update: Optimal file structure for React applications 1.1

Nine months ago, I published an article titled “Optimal file structure for React applications,” which has since become my most widely read article ever written. I published it with the promise that it would be a living document, updating as best practices are discovered, described, and justified. Today, I have updated it to reflect what has worked best for the AWS CloudWatch Logs team in shipping the new design of the console.
This article will discuss the changes to the optimal file structure for React applications. You can read the remaining best practices as well as these changes in the original article.
Flattened asset directories 🥿
Originally, assets, such as images and SASS mixins, were put into an assets
directory, e.g. assets/images/image-name.png
. It offered no benefits to have an assets
top-level directory and only served to cause confusion when attempting to categorize non-asset files, such as Redux reducers. For extensibility purposes, this directory has been flattened to just images
, scss
, reducers
, or whatever category you need. Your directory structure is now simply src/CATEGORY/module-name
.
Directory barrels 🎁
Directories that contain JavaScript imports are given “barrels,” or index.js
files that re-export the contents of that directory. The most clear implementation of these are src/components
and src/utils
.
// src/components/index.js
export { default as App } from './app';
export { default as Breadcrumbs } from './breadcrumbs';
export { default as Button } from './button';// src/utils/index.js
export { default as authenticateUser } from './authenticate-user';
export { default as isLoggedIn } from './is-logged-in';
export {
default as reduceLogGroupsToServiceNamespaces
} from './reduce-log-groups-to-service-namespaces';
These directory barrels will come in handy later by allowing us to jest.spyOn(category, 'exportName')
during testing to mock responses or validate dependency calls with a significantly simpler syntax than mocking the entire module.
Packages directory 📦
The packages directory is explicitly given a section to discuss a directory dedicated to modules that are re-usable outside of the current application. It is easiest to think of these as open source modules still in development. While they are not open source yet, they will be eventually, and they enforce a mindset that they should not be tightly coupled to the business logic of the application. If your company does not publish open source code, these modules may still be published internally for other teams (and external to the application itself).
Subcategory restructure 🥪
Subcategories (or categorized code that are uniquely dependencies of a parent module) are put into category directories and flattened to a max depth of 1. This makes more sense by example.
Previous directories:
PARENT MODULE: src/components/my-componentDEPENDENCIES:
src/components/my-component/icon/icon.js
src/components/my-component/icon/icon.scss
src/components/my-component/icon/index.js
src/components/my-component/icon/icon-svg/icon-svg.js
src/components/my-component/icon/icon-svg/icon-svg.scss
src/components/my-component/icon/icon-svg/index.js
src/components/my-component/use-some-hook/index.js
src/components/my-component/use-some-hook/use-some-hook.js
The dependencies Icon
and useSomeHook
were put into directories within MyComponent
, because they are uniquely dependencies of MyComponent
. The dependency IconSvg
is put into a directory within Icon
, because it is uniquely a dependency of Icon
.
Current directories:
src/components/my-component/components/icon/icon.js
src/components/my-component/components/icon/icon.scss
src/components/my-component/components/icon/index.js
src/components/my-component/components/icon-svg/icon-svg.js
src/components/my-component/components/icon-svg/icon-svg.scss
src/components/my-component/components/icon-svg/index.js
src/components/my-component/components/index.js
src/components/my-component/hooks/index.js
src/components/my-component/hooks/use-some-hook/index.js
src/components/my-component/hooks/use-some-hook/use-some-hook.js
This new pattern follows the same structure you would find in src
-- a directory structure based on category, not module name. These new category directories allow us to use the same index.js
“barrel” discussed earlier. When testing MyComponent
, we can now mock useSomeHook
to test the view and container separately, even though it’s a single component. We can mock the child components so that they do not make API calls or cause side effects during the test suite of MyComponent
. The overall navigation of the my-component
directory becomes simpler in that it matches an existing rule — the same used for src
.
You may also notice that IconSvg
is no longer a child of Icon
, even though it is uniquely a dependency of Icon
. This is what it means for the subcategories to have a max depth of 1. Your file paths are now capped at src/CATEGORY/module-name/SUBCATEGORY/sub-module-name
. This benefits the code review process when src/CATEGORY/deeply/SUBCATEGORY/nested/SUBSUBCATEGORY/modules
can become hard to follow and digest.
Hooks 🎣
The previous article did not do much besides mention hooks in passing. As shown in the subcategory restructure, we now explicit a place for hooks — in the hooks
directory. This applies both to src/hooks
and src/components/some-component/hooks
. The ability to put hooks into barrels has done wonders for separately concerns during the test process. When I jest.mock(hooks, 'useSomeHook')
to return { loading: true }
, I expect my component to render a loading spinner. I do not have to setup, render, and teardown the entire application to render a loading spinner. I do not have to make or mock an API call to make sure that my loading spinner appears. Most importantly: I do not need to know the implementation details of my hook to make sure my component can accurately display a loading state. If my hook changes its API call, I should not have to update every test suite of every component that uses that hook. The barrel hooks
directory is a miracle.
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.