How I Structure My React Projects
Recently, my friend Chance and I had this exchange on Twitter:
Do you shrug and go “it really doesn’t matter, just pick one and be consistent?”
— Kyle Shevlin (@kyleshevlin) June 11, 2021
Cause that’s what I would do.
It’s true. It really doesn’t matter. That said, I’m going to share a few of my preferences and why they are what they are.
Warning: These are my opinions. You don’t have to agree with them. If you don’t agree with them, that’s ok. Just know, I’m not going to engage in an argument with you about these opinions.
Single file components
Don’t have a folder named MyComponent
with an index.tsx
, styles.ts|css
, and types.ts
file, etc.
Do have a file named MyComponent.js|jsx|tsx
and have all related styles and types in the same file.
Why?
First, it is a waste of time and annoying AF to have to search for a file in my editor by typing MyComponent
followed by index
to ensure I get the right file.
This is one of my least favorite features of “convention over configuration” frameworks like Ember and should be avoided in React projects. You end up with hundreds and hundreds of files that have the same name, index.js
or component.js
and you always have to do two-part searches. What an absolute waste of keystrokes. I know you may think that’s trivial, but it adds up when you’re searching for dozens and dozens of files every day. It’s never good enough to just do MyComponent
. You need to add styles
or types
or use arrow keys to navigate a list to get the file you were looking for. What a waste!
Second, having files that only exist to export tightly coupled information, like styles
and types
is equally wasteful and can lead to some organizational footguns. Put the info directly in the same file. You’ll convey information faster, improve Intellisense speed, and more. Plus, you get the bonus of not creating an export unless other modules actually need that value. When you do the multi-file approach, you run the risk of people importing styles and types in an ad hoc fashion and it’s a fast way to running into duplication and organizational messes.
Sub-components in same file, too
Don’t make a separate file for a small sub-component that won’t be reused by another part of the system.
Do use sub-components where it makes sense, but keep them in the same file.
Why?
Related to single file components, if you are breaking up a bigger component into sub-components and those sub-components are relatively small and are not intended for reuse, keep those in the same file as well.
function BigComponent(props) {
return (
<div>
<SubComponentA />
<SubComponentB someProp={props.someProp} />
</div>
)
}
function SubComponentA() {
return <div>Sub A</div>
}
function SubComponentB({ someProp }) {
return <div>{someProp}</div>
}
So long as SubComponentA
and SubComponentB
have no reason to be exported or used elsewhere, there’s nothing wrong with keeping them within the same file. In fact, it encourages you to break your monolithic components down into reasonable and understandable pieces.
There is one downside to this if you’re using TypeScript. You will have to re-declare the types of your props
for every component you do some prop drilling for. Given that you can often define a Props
type, and then reuse the types in it with Pick<>
or something similar, I find this to be a reasonable tradeoff.
Feature folders
Don’t just have a folder named components
and throw everything in there.
Do have a folder named features
where each sub-folder is a specific feature containing all the files related to that feature.
Why?
I think when you’re first starting a project or doing a side project, it’s fine to have a single components
folder and throw everything into it. But soon enough, you’ll realize you have a set of components that are shared
and many components that are really just one-off abstractions for a single feature. Keeping these in the same folder misrepresents how these components should be used in your system.
I have found it better to take those components related to a single feature to give that feature a name and contain all the files for that feature in that folder. Doing so, allows you to treat the feature similar to a package, exporting precisely what you intend to the rest of your system. You can even write eslint
rules that prevent people from importing values that are nested further than the top level of your features
folder. It forces you to think about the interfaces of your system and to define useful boundaries.
This also has the benefit of more accurately representing your organizational information. I have found this to be a useful means of helping people onboard to complex projects. Rather than asking them to try and piece everything together from across your codebase, it is easy to pick a feature and explore that sub-tree of your system.
Summary
Don’t create extra files just to export something that’s only used by a single component, like styles and types. Use single file components to encourage exporting precisely what the rest of the system needs and improve file search by a few keystrokes each time.
Similarly, keep sub-components in the same file until they become useful abstractions for the system.
When you do have these useful components to break out, determine if they belong to part of your shared
system , or really belong solely to a feature. Use feature folders to identify boundaries in your system.