π Nominal & Structural React Architecture β Part I
Defining an ultimate React project structure through breaking the habits and borrowing from fellow frameworks. Here, in particular, we're gonna touch the "nominal" side of this process.
Table of Contents
πββοΈ Motivation
As time passes, we get acquainted with at least several approaches to build the βarchitectureβ of a React application, one of which being no approach at all, as Reactβs development team does not impose any ruleset on how to structure the application's code on us.
Yet, there are plenty of abstractions devoted to standardize it, like:
- "scoping" modules into
public
/private
folders; - classifying the components as
container
/presentational
; - incorporating atomic design through
atoms
/molecules
/organisms
; - following the classical approach seen in
create-react-app
:components
/pages
/styles
, etc.
All of these try to solve the fundamental struggle of our brain to create an order out of chaos. But none of them could stop the enthropy of scaling. None could provide a way to get rid of unneeded abstractions or vendor lock-ins. Moreover, the majority of the attempts to create an ideal structure tend to fall down the issue of thinking too much about the concept itself, not the benefits it should provide us, the developers, who wonder around the project every single day, add new features, refactor/fix the existing ones, and generally spend much time looking for something in the file tree.
Having studied what well-known and broadly used frameworks, like Angular on the front-end and NestJS on the back-end had to offer, having read many opinions around this topic, and having formed my own opinion, I propose you to follow me on the next adventure: forget what you know so far and open your mind to something seemingly unusual in the React world.
Considering the alternatives wonβt harm, but rather widen your overall view on the topic and strenghten the ability to pick the approach which works best for you.
π· Naming Scheme
There are usually just two things to boggle the developer's mind on an everyday basis, one of them being naming things. Following the consistent and clear naming scheme convention (over configuration) is what makes our code much more readable, understandable and extendable.
The "code" part was completely covered in the famous Clean Code book by Robert Martin. And I tend to not argue that the same could apply to how we name our files and folders.
Especially, I'd like to point out 2 main points to support the need for convention:
- Simplicity. I don't wanna think of/choose which way to name the file/folder every time I need to do it.
- Aesthetics. Consistent naming all over the project makes it look kinda nicer (well, at least to me).
To start off, we have to pick a naming scheme to which we will stick to thoroughly from head to toes.
I'd like to define the requirements which would lead us to believe the scheme to be right one for the job:
- Contextually independent of the code, which means the code inside the file or folder should not dictate its naming scheme.
- Easy to read in [almost] all cases.
π Candidates
Now, let's take a quick look at what naming schemes we have in our tool-belt.
const candidates: string[] = [];
Pascal Case
PascalCase
. Commonly seen in the React-land, largely due to JSX's requirement of naming components in pascal case.
Cannot be considered a candidate for the convention, as it's too tightly coupled with the JSX contextually. Also, in many occasions pascal case would become hard to read, e.g. ControlIcon.tsx
, WaterfallItem.ts
, etc.
#1 β β, #2 β β.
Camel Case
camelCase
. Another example of a name casing having come to us from the JS-land.
Once again, its context does not allow for considering camel case to be the candidate. There could also be cases with visually conflicting combinations of letters, like useRecallInstance.ts
.
#1 β β, #2 β β.
Capital Case
CAPITAL_CASE
. Mostly used for naming shell/environment variables, capital case is undeniably hard to read. On the other hand, it can be considered contextually independent, as environment variables are kind of a thing you won't encounter that often.
#1 β β , #2 β β.
Snake Case
snake_case
. This one's interesting, as in JavaScript and React it is practically impossible to find examples of snake case, as opposed to some other languages like Rust and Python, which we're not taking into account here.
Is it hard to read? For me, the answer is no. You always get lower case characters, and the underscore separator provides a clear distinction between the words, nullifying the possibility of similar letters colliding with each other.
#1 β β , #2 β β .
candidates.push('π');
Kebab Case
kebab-case
. The characteristic above applies to the kebab case as well. It both cannot be found in JavaScript (since the syntax itself does not allow for the "-" to be used in variable names), and is easy to read.
It's worth mentioning, though, that we could still come across kebab case is the names of web components, but I'd not consider this relevant enough.
#1 β β , #2 β β .
candidates.push('π₯'); // not quite the kebab looking emoji, but will do
π vs π₯
Now, let's try to compare seemingly similar cases and pick the winner.
For me, there's one critical point in which those two are clashing. This is the occurence in the ecosystem.
Both aforementioned Angular and NestJS use and popularize the kebab case. I suppose, kebab case comes from the Angular's style guide of naming component selectors, which NestJS adopted, being heavily inspired by the former.
Also, for those who use keyboard for navigation, the presence of hyphens in the phrase allows for selecting the words separately though β§
β₯
β
/ β§
β₯
β
, which is impossible with snake case, as lower dash is considered to be a part of a word.
So, without further ado, I'm proposing the kebab case to be the ultimate and only naming scheme for us to use π₯³.
π File Suffixes
If you have not already guessed, this system was heavily inspired by what NestJS and Angular have to offer in terms of a naming convention.
Although file suffixes may seem very unfamiliar and strange in the React world, I'll try my best to outline why they are what makes this system even better.
We've already decided on using kebab case for all the files and folders. Before it, we had every React component have at least a .jsx
/.tsx
extension indicating the presence of the JSX, along with the pascal case naming further supporting this to actually be a component.
Now, we're left with only the extension, which is not enough to assure us that it actually is a component, since any file with JSX inside must have an x
in its extension (it's not true for JavaScript, but would still be preferable).
π Overview
But, anyways, why would the look/context of the file name describe what it is, and not its actual name?
Try to guess what this file name stands for β button.component.tsx
? Yeah, that's right, a Button
component!
The concept further expands on other "kinds" of files we make every single day:
.component
β for any valid React component, be it a traditional functional/class component, or a component created through HOCs/HOFs like thestyled
function ofstyled-components
(for which.ts
extension suffices).button.component.tsx
..styles
β any kind of styles: CSS, SCSS, LESS, JSS, CSS-in-JS, etc. The difference is in the extension, not in the name itself.button.styles.ts
..page
β a top-level page-component rendered upon visiting a route.login.page.tsx
..props
β a component's props definition (usually.d.ts
in TypeScript).login.props.d.ts
..store
,.reducer
,.state
,.actions
,.selectors
,.saga
, etc β for all those users of Redux, Redux-Saga and others.app.store.ts
,app.reducer.ts
,app.state.ts
,login.actions.ts
,login.selectors.ts
,login.saga.ts
..config
β for configuration files.router.config.ts
..spec
,.test
,.e2e-spec
,e2e-test
β test files.button.spec.tsx
.
Those are the ones I myself often use, but it doesn't mean that there's no more of them. You can always come up with your own suffixes for your use-cases, as long as you stick to the system and try to be as transparent and clear as possible.
π€·ββοΈ Why bother?
Now, let me explain what problems do we have without the suffixes, which they're solving pretty easily:
Expressiveness
Although the suffixes may seem hard to type every time you have to create a file, they are still pretty expressive at what code that file is hosting inside. Every other time you will look at it β you'd know right away what it is.
Also, there are plenty of tools to automate the process of creating the files through predefined templates, like WebStorm's File and Code Templates:
Classification
Although I'll cover this topic more thoroughly in another article, it is important to admit that a single feature assembled out of different files can further be classified down to what smaller features it consists of, like a component, its styles, props, config, etc.
Identification
Have you tried launching a search interface in your favourite IDE (or file system) and looking for something in, for example, the styles? Let me illustrate what it usually looks like without the suffixes:
Yup, they all have the same name, although coming from different locations. Try to guess where each of those belongs.
The same applies to all other names that do not uniquely identify the file. They may look ok in the context of the folder they belong to, but in a context of a full application this concept becomes dangerous.
It can look nice, though, if the file system viewer somehow indicates the folder which hosts each particular file. But if it doesn't?
Independence
.component
and .styles
suffixes provide us with the same independence we sought in the file name cases: component can not come from the React itself, and styles can be of any technology or tool. We have to rely just on the name and not on the actual implementation.
Consistency
We use suffixes day-to-day, maybe even without noticing.
Many tools, like eslint, prettier, babel, webpack, Next.js, etc have an option (or even a requirement) to name their configs with the .config
suffix.
Test files always have a .test
or .spec
suffix so that the test runner can distinguish between the test and implementation files.
The same goes for the .module.css
files which tell the compiler to scope the containing styles to the module they belong to.
So why not follow the same style for all the other files out there?
Convenience
There are plenty of tools that do something like linting reliant on the file names. In my experience, configuring such tools is a breeze, when the files are consistently named using suffixes. It all comes down to just picking them though simple regular expressions, like *\.component\.[tj]sx?
.
π Real-world example
Having written several full-stack applications β one of which being this very blog itself β I must admit that suffixes work quite beautifully in React, especially in conjunction with NestJS hosted in the same repo.
The file structures of both front-end and back-end are looking alike, the concept and mental model do not change when changing the "-ends" and the work becomes the pleasure.
I strongly recommend looking at the source code of this very site and get a grasp of what this naming system looks like in a real-world application. With time, it will definitely grow on you, however strong your past habits are.
In Part II we're gonna cover the approach to structure the project in a way you'd fall in love with how easy it is to navigate it, find stuff quickly and have close to none thoughts on where to place the module you are to create next.
π€ TL;DR
- Use only the
kebab-case
for naming files and folders. - Use suffixes in file names to indicate which kind of file this is:
button.component.tsx
,button.styles.css
,login.page.tsx
,login.reducer.ts
. - Live example.