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.
I would like to introduce the different structures I used first and then share the context of these philosophies with you.
.
|-- 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 many problems during writing this project.
.
|-- .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.
form
and layouts
, 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 components that have 50 to 75 lines of this kind of import and it is very chaotic.
.
|-- .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.
components/ui
folder.MemberIntro.tsx
is a disposable component that will only be used by its parent OurMember.tsx
so it should stay in the
components/about/OurMember
folder not in the components/ui
folder.
This act can make naming components much easier compared to treating every component as general purpose components and putting them into the components/ui
folder.Nav
will be exported to the /components/ui
folder and the OurMember
component will be exported to the /components/about
. In this way, the import code will look cleaner./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 to 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"
.
|-- .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.
This is a unique way to organize our design system. Overall it brings some fresh air into our projects and serves our needs well.
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.
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.
This is the mistake I made at instill-ai/[email protected]
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.
When your file has 30 to 50 imports, it will be messy if the imported function or component is not exported to its 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.
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
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.
Also published here.