Colocating React component files: the tools you need
React does not force any file structure to work. File structure is one of those themes up for debate where no choice can be the gold standard.
In general, it is a good idea to keep files that often change together close to each other. This principle is called “colocation”. (React docs)
This post is not about why you should or shouldn’t colocate. That’s up to you. We do colocate our component files. That’s our choice. So, this post is about how we do it and how we configure our tools to support it.
Our current component layout
We put each component in its own folder. This folder is named after the component using kebab-case. Casing is not relevant. I won’t debate whether is best to use kebab, snake, pascal, camel or any other casing you may love. Component folders are arranged inside other folders to define a taxonomy.
Each component folder contains several files, also named after the component. Each one with its own distinctive extension:
.jsx
for the component’s code. The component is always thedefault
export..module.css
for the component styles. We use CSS modules and BEM conventions..test.jsx
for the component’s tests. Jest is our testing framework of choice..test.jsx.snap
for the test snapshots.
We also add an index.js
file that re-exports the component to have shorter import sentences: import Component from "path/to/component";
instead of import Component from "path/to/component/component.jsx";
.
Example, a simple button component
Our SimpleButton
will be grouped under buttons
and has the following layout:
components
└── buttons
└── simple-button
├── index.js
├── simple-button.jsx
├── simple-button.module.css
├── simple-button.test.jsx
└── simple-button.test.jsx.snap
simple-button.jsx
defines the component. In this example a SimpleButton
that can be either enabled or disabled:
simple-button.module.css
contains the CSS modules for the component. Selectors follow our own JS friendly BEM naming convention:
The index.js
re-exports the SimpleButton
component from simple-button.jsx
:
And, lastly, simple-button.test.jsx
contains the Jest test suite for the component. Notice we import SimpleButton from "."
. This way we make sure the index.js
re-export is working as expected.
Colocation, following this layout, gives us several advantages:
- Component files are easy to find.
- Trivial imports from test to component, component to assets, etc.
- Easily change the components taxonomy. To move a component, just move its folder around.
- Easy extraction of components to their own packages. Once again, you just need to move the folder around.
Depending on which tools we are using to document our components we can have more colocated files (.md
or .mdx
for example. We use both React Styleguidist and Docz, depending on the project). We are not covering this topic today, but it also benefits from colocation.
Configuring the tools to fit this layout
Your everyday JS tools does not encourage colocation by default, or, at least, not this way. Don’t worry, making them work the way we want is just a matter of tinkering with the config.
Babel: allowing default exports
The current standard syntax for export
does not allow to re-export from another package the way we do in our index.js
. It needs more boilerplate code unless you use Babel and @babel/plugin-proposal-export-default-from. It implements the stage-1 proposal to support the export from
syntax.
Webpack: supporting CSS modules
To use CSS modules, webpack must use the css-loader
with modules enabled. We setup the loaders for the .css
imports overriding the .module.css
imports to use CSS modules. This way we can import some third party stylesheet or setup some global styles avoiding the CSS modules scoping.
Jest: collocating test and snapshots
By default Jest will look for your component test in a __test__
folder and store snapshots in __snapshots__
. You can change this behaviour using the testRegex
and snapshotResolver config options. With testRegex
you state that your test will be named after the component file and use a .test.jsx
extension.
The custom snapshotResolver
tells Jest to store the snapshots in the same folder as the test, adding a .snap
extension.
Jest: supporting CSS modules
If you use CSS modules you must tell Jest how to handle your .css
imports by configuring a moduleNameMapper.
We use identity-object-proxy. The styles
you import in your component will be an ES2015 proxy that returns the keys you use as a string: styles.Button
will be "Button"
. This way the classNames
in the snapshots will be meaningful.
ESLint: applying rules only to test files
Last but not least, we use ESLint to check our code. We can apply certain linting rules only to our test files using the overrides
option. We use it to enable Jest globals and setup some eslint-plugin-jest rules.
Summing up
Colocating component files brings several advantages. We strongly believe that it is the better approach and we are happy with it. You can develop your componentes following this layout too, just by making simple changes to your tooling configuration. Give it a try. You won’t go back.