React component directory structure
As a team we are fairly new to React so when we began on a new React project a couple of years ago we did not have any experience to guide us on how to organize our components directory structure. Our initial agreement was that each component consisting of sub-components would be in its own directory with its sub-components and if a sub-component consisted of multiple components itself, then it would be placed in a sub-directory within its parent’s directory.
Our plan was to organise the code like this:
SomePage
SomePage.tsx
Header
Header.tsx
Search.tsx
Filters.tsx
Body
Body.tsx
List.tsx
Overview
Overview.tsx
OverviewHeader.tsx
OverviewBody.tsx
Simple, right? Directories are self-contained, have a very low risk of becoming bloated and you can clearly see the hierarchy of the page from the directory structure. However, this post would not have been written if we found that this was the optimal solution. In practice, however, things didn’t work out as well as we hoped and we ended up with beasts like this:
Layouts
index.tsx
ListAndOverview
ListAndOverview.tsx
List
List.tsx
SummaryTable.tsx
Overview
Overview.tsx
DetailPane
DetailPane.tsx
MultipleDetailPane.tsx
SingleLayout
SingleLayoutDetails.tsx
SingleLayoutHeader.tsx
SingleLayoutLocations
SingleLayoutLocationsMap
SingleLayoutLocationsMap.tsx
SingleLayoutLocationMarker
SingleLayoutLocationTable.tsx
MultipleLayouts
MultipleLayoutDetails.tsx
MultipleLayoutHeader.tsx
Problems
Now believe it or not such a nested structure has some drawbacks. Firstly and most importantly to us, this kind of nesting obfuscates the contents of the page. If you were to look in index.tsx
the render function would look something like this:
render() {
return <ListAndOverview {…this.props}/>
}
This provides a very limited view of what the page does/looks like and to get to what the page actually displays requires a lot of jumping from one component definition to another which results in the developer expending their mental effort trying to put all the little pieces together into one coherent picture.
Secondly, we were forced to have a number of small, almost pointless, components, just for the directory structure to still communicate the structure of page.
While the earlier point that with this kind of nesting you can deduce the makeup of the page from the directory structure still stands, we felt that the code should speak for itself and that it would be easier to work it out from the code. Thus, we decided to completely get rid of the nesting and try to get the code to communicate the component structure.
Results
Layouts
index.tsx
LayoutDetailPane.tsx
LayoutDetails.tsx
LayoutHeader.tsx
LayoutLocations.tsx
LayoutLocationsMap.tsx
LayoutLocationsTable.tsx
...
The above directory structure addressed the complexity of the import statements however the bigger change was within the index.tsx
render function.
render() {
return this.projectPage(
this.layoutsList(
LayoutsListHeader(
this.layoutSearch(),
this.layoutFilters(),
this.layoutDrawers()
),
this.layoutsListBody(
this.layoutsSummaryTable()
)
),
this.layoutOverview(
MultipleLayoutDetailPane(
this.multipleLayoutHeader(),
this.multipleLayoutLocationsMap()
),
LayoutDetailPane(
this.layoutHeader(),
this.layoutDetails(),
this.layoutLocations()
)
)
)
}
The page contents are clear from the highest level component. It’s easy to work out what individual components are made up of and where they are in the page hierarchy.
The individual components got simpler too. Take LayoutDetailPane.tsx
, for example, which went from this:
<div>
<Grid container>
<Grid item>
<SingleLayoutHeader
selectedLayout={this.props.selectedLayout}
/>
<SingleLayoutDetails
selectedLayout={this.props.selectedLayout}
/>
</Grid>
<Grid item>
<SingleLayoutLocations
selectedLayout={this.props.selectedLayout}
/>
</Grid>
</Grid>
</div>
To this:
<div>
<Grid container>
<Grid item>
{headerComponent}
{detailsComponent}
</Grid>
<Grid item>
{locationsComponent}
</Grid>
</Grid>
</div>
Now that the DetailPane component gets its sub-components from index.tsx
, it is clear that this component’s job is simply to arrange those sub-components in a grid regardless of what they are. This agnosticism makes it much more robust to changes outside of the component.
Conclusion
Our new approach:
- Improved the clarity and usability of the code, both at the top level and within lower level components
- Having simpler components made them easier to test so 15 new tests were added which improved our project’s code coverage by 0.3%
- Decreased the number of files comprising the Layouts page from 22 to 18
- Decreased the number of Layouts page sub-directories from 11 to 2
- Decreased the number of lines by 136 (even with the addition of new tests)
In short, a clear win on all fronts with a single minor drawback of the directory structure having a higher risk of becoming bloated.
This example shows that any well intentioned upfront design inevitably benefits from refactoring. This doesn’t mean that it was wrong to do the upfront design, if we hadn’t taken he time to define an upfront structure we would most probably have ended up in a complete mess. However as you work with a structure you learn about what works and what doesn’t and the step to feedback these lessons into a refactored design is essential.