Don’t over emphasize the importance of the react folder structure, focus on the philosophies beyond the folder structure
Sep 06, 2022
Managing proper react component folder structure is hard, it needs to be flexible, extendable, and most important of all, it needs to make naming a component as easy as possible.
I adapted several react folder structures across 3 different repo this year, what I discovered is not a steady structure but some philosophies behind the structure.
- Structure should be consistent and the principle should be documented.
- Component’s materials should stay near to the component itself.
- It’s a good practice to export your component to the outer responsibility layer.
- It’s a good practice to limit the length of the filename to 3.
I would like to introduce the different structures I used first and then share the context of these philosophies with you.
Pnyx
.
|-- public
|-- src
| |-- config
| |-- lib
| |-- locales
| |-- pages
| |-- shared-hooks
| |-- static
| | `-- images
| |-- types
| |-- utilities
| |-- components
| | |-- buttons
| | |-- layouts
| | |-- links
| | |-- Footer.tsx
| | |-- Header.tsx
| | `-- ...etc
| |-- App.tsx
This is a simple project and most of its pages are static. As a first-time React user, I learned this structure from videos on Youtube. The whole repo was maintained in a flat manner. There was no nested folder with a duplicate name like /components/PageHeader/PageHeader.tsx
and I didn’t export any component to its outer layer. Because the codebase is relatively small I didn’t observe much problems during writing this project.
instill.tech@0.1.6
.
|-- .github
|-- .husky
|-- .storybook
|-- .__mocks__
|-- .__tests__
| |-- ui
| | |-- Headline.test.tsx
|-- contexts
|-- .hooks
|-- .lib
|-- markdown
|-- pages
|-- public
|-- stories
| |-- ui
| | |-- Headline.stories.tsx
|-- styles
|-- components
| |-- forms
| |-- layouts
| |-- ui
| | |-- blocks
| | |-- buttons
| | |-- groups
| | |-- links
| | |-- Headline.tsx
| | |-- SubHeadline.tsx
| | `-- ...etc
After further exploring and digging deeper, this project has several changes compared to Pnyx.
- It adapted storybook and stored stories in a different folder,
- It had the test suites and stored the test files in a different folder,
- Component folder had categories like
form
andlayouts
, it grouped the same components together.
This structure was experimental and I had not formed a steady opinion of React folder structure at that moment. But back then the idea of separating component material(like stories and tests) into different folders caused a lot of trouble. When I refactored some minor changes beneath the component folder, I needed to adjust the /tests
and /stories
folder too. These folders became inconsistent over time.
Besides, those components were not exported to the outer level, so it was hard to digest them. Every import of a component will occupy a line of code and it looks messy.
import ClientBlock from "@/components/ui/blocks/ClientBlock"
import Headline from "@/components/ui/Headline"
Image some Next.js page component that have 50 to 75 lines of this kind of import and it is very chaotic.
instill-ai/instill.tech@0.13.0
.
|-- .github
|-- .husky
|-- .storybook
|-- docs
|-- public
|-- release-please
|-- src
| |-- contexts
| |-- hooks
| |-- styles
| |-- types
| |-- lib
| |-- pages
| |-- components
| | |-- about
| | | |-- OurMembers
| | | | |-- OurMembers.tsx
| | | | |-- MemberIntro.tsx
| | | | `-- index.ts
| | | `-- index.ts
| | |-- career
| | |-- docs
| | |-- landing
| | |-- policy
| | |-- ui
| | | |-- Nav
| | | | |-- Nav.tsx
| | | | |-- AboutPageLink.tsx
| | | | `-- index.ts
| | | |-- PageHead.tsx
| | | `-- index.ts
This is the current version of my favored folder structure.
- Component’s materials are near the component itself.
- General purpose components will stay in the
components/ui
folder. - Non-general-purpose components will stay in the parent’s component folder.
- For example,
MemberIntro.tsx
is a disposable component that will only be used by its parentOurMember.tsx
so it should stay in thecomponents/about/OurMember
folder not in thecomponents/ui
folder. This act can make naming components much easier compared to treating every component as general purpose components and putting them into thecomponents/ui
folder. - All components are exported to its outermost responsibility folder. For example,
Nav
will be exported to the/components/ui
folder and theOurMember
component will be exported to the/components/about
. In this way, the import code will look cleaner. - Under the
/components
folder, the components are categorized by their responsibility not their functionality. For example, there won’t be a category like/block
and/section
but/onboarding
,/career
, and/about
. In this way, we could limit general-purpose components in just one place and other components are treated like disposable components under their responsibility folder.
import { Nav, PageHead } from "@/components/ui"
import { OurMember } from "@components/about"
instill-ai/design-system
.
|-- .github
|-- .husky
|-- .storybook
|-- public
|-- src
| |-- hooks
| |-- styles
| |-- types
| |-- utils
| |-- index.ts
| |-- ui
| | |-- index.ts
| | |-- Buttons
| | | |-- ButtonBase
| | | | |-- ButtonBase.tsx
| | | | |-- ButtonBase.stories.tsx
| | | | `-- index.ts
| | | |-- SolidButton
| | | | |-- SolidButton.tsx
| | | | |-- SolidButton.stories.tsx
| | | | `-- index.ts
This project is a special case when it comes to folder structure. It uses the structure I called “Base and Export” two-level component design.
- Base-level components: The foundation of every type of component.
- Base level components won’t be exported to be used by exterior consumers.
- Base components will define all the props and functions that the component needs.
- Exported-level components: Inherits all the props from a base component but may have a predefined style, meant to be used by the consumers.
- Exported-level components will be consumed with the limited props like onChangeInput, borderStyle, fontFamiliy, or other functional props.
- We will limit as few style props an exported component needs as possible, to control the visual style of the component.
This is a unique way to organize our design system. Overall it brings some fresh air into our projects and serves our needs well.
Philosophies
During this journey, what I learned the most is “There is no one fit for all folder structures”. If you encounter someone claiming that, it will be better to leave them there and move on. The reason is quite simple: The technology we use is drastically changing. For example, Next.js has a new layout RFC that will change our folder structure a lot, and the adaption of storybook changes the way we thought about components too. I foresee that we will have more frameworks and libraries like these.
Structure should be consistent and the principle should be documented.
This is my first point. The codebase is not a single person’s playground but a collective intelligence that the maintainers had to agree on. No matter what you adopt, you should make the structure as clear and consistent as possible. In this way, other maintainers will have a clear path to look into your project and once you need to refactor the whole repo to adopt a new tech it will be easier to migrate a consistent structure.
Component’s materials should stay near to the component itself.
This is the mistake I made at instill-ai/instill.tech@0.6.1
that caused lots of trouble and inconsistency when I tried to refactor its structure. Overall I think we should put our component’s materials close to the component itself. Not only to reduce the overhead of refactoring but also to help others find your component. In this way we could help the maintainers to name their components easier too, because they don’t need to come up with a component name that can describe itself in various contexts. It only needs to describe itself under the context of its parent.
It’s a good practice to export your component to the outer responsibility layer.
When your file has 30 to 50 imports, it will be messy if the imported function or component are not exported to their outer folder. Imagine you have 30 lines of this kind of code.
import ClientBlock from "@/components/ui/blocks/ClientBlock"
import Headline from "@/components/ui/Headline"
Compared to this way.
import { ClientBlock, Headline } from "@/components/ui"
I think the latter is better.
It’s a good practice to limit the length of the filename to 3.
This philosophy comes from a friend of mine. Because we are using the PascalCase for the naming convention of our components, it may be hard to read when the length of words exceeds 3.
CallToAction.tsx
and CallToActionBlock.tsx
Don’t over-emphasize the importance of a structure
The ideal structure is changing and changing fast, and it highly relates to the technology we adapt to. I think the best method is to maintain these kinds of fundamental philosophies and adapt new tech and structure in a quick and flexible manner.