React

Architecting React Apps Like it's 2030

July 18, 2022 • 9 min read
Architecting React Apps Like it's 2030

There is one problem that every React Developer falls into during his journey. This is how to structure a good app architecture.

This article will help you avoid some common errors that most of us make architecting react applications and will give you the right way to structure your directories.

Is This For You?

Before starting, it’s necessary to underline a point: there is no perfect solution that fits any possible case. This is particularly important to understand because a lot of developers are always looking for the only one solution to all their problems, I’m sorry to say that if you are looking for this, this could not be the article for you.

Time to Architect!

If you have arrived here, it means that you are interested in the topic, so, it’s finally time to start! All the content I will mention will be put into an src directory, and every mention of new folders will be relative to this constraint, keep it in mind.

Components

What are the first things a React Developer creates in a project? I would say components because you know, React apps are made with components so, no components no party.

During my career, I saw a lot of different architectures (some very good, and others awful..) and I figured out one path which can be used in most cases, even for little projects.

This is what it looks like:

├── components
   ├── common
   └── button
       ├── button.tsx
       ├── button.stories.tsx
       ├── button.spec.tsx
       └── index.ts
   └── signup-form
       ├── signup-form.tsx
       ├── signup-form.spec.tsx
       └── index.ts

The key point here is the following: we have components that contain all the components that are used more than one single time in the app, so we are going to exclude every feature-specific component from this folder.

Why? Simply because the meaning of this folder is to contain reusable logic. And I put also a difference between global and scoped reusable logic. A button is supposed to be used on almost every page of our app, that’s why a common directory exists. Something different happens instead for the signup-form component, why is this reusable?

Well, let’s suppose to have two different pages (more on this later) for sign-in and sign-up, this component needs to be repeated two times, that’s the reason why is put into the components folder but as a scoped logic.

Notice how as I said before, this is a specific case, if we had a single page for authentication, we shouldn’t have put it in here.

Some examples of what can be inserted into the common folder:

  • Inputs
  • Cards
  • Alerts

I think you got the point.

You probably noticed also that every single component is placed into a proper directory with a very easy-to-understand naming convention.

button
├── button.tsx
├── button.stories.tsx
├── button.spec.tsx
└── index.ts

That’s because your app can eventually contain more than 1000 components, and if all of them have a test or a storybook file, this can easily become messy. Let’s explore some key points of this folder:

  • All the component-related files are in this folder.
  • All the exportable modules are put into an index.ts to avoid the awful double name in import.
  • All the files are named in kebab-case.

I know it seems a little bit verbose, especially for newbies or for little projects, but it requires very little effort and as a return to having a gain in code readability, want an example? Try to answer these questions:

  • Where is the button component? -> In the button folder.
  • Where are the stories for this button? -> In the button folder.
  • Oh dear, I need to find the test for this button where I can find it? -> Answer by yourself.

Again I repeat, if you feel these questions are silly and obvious, the day will come when you will work on a code base where best practices are the last thing that was considered and you will remember this article.

We’re not done with the components yet, but we’ll come back to that later.

Pages

Let me tell you a secret, in React, pages do not exist. They are components too, composed with, well, other components. But differently from the other components, usually are very strictly scoped (in a specific URL path for example). Where do we go to insert them?

We can use a practical views (or pages if you prefer) directory, in which put all those stuff, have a look a the example:

views
├── home.tsx
├── guestbook.tsx
└── newsletter
    ├── index.ts
    ├── newsletter.tsx
    └── components
        └── newsletter-form
            ├── newsletter-form.tsx
            ├── newsletter-form.spec.tsx
            └── index.ts

For the home and guestbook it’s fairly simple, a page is supposed to be the result of the composition of other components, which have proper tests, so I’m not gonna create a specific directory for them.

The case is different for the newsletter page, which has something specific, a newsletter-form component. In this case, I use the approach of creating a nested component folder inside the page folder and act like I’m in the normal components folder, so using the same rules.

This approach is powerful because lets you split the code into small chunks, but keeps the architecture well organized. The newsletter-form component should not be put into the “main” components folder, simply because here is the only place in which is used. If the application grows, and the component will be used in several parts, nothing prevents you from moving it.

Another tip i usually suggest is to keep a consistent name between the page and the route, something like this:

<Route path="/bookings">
  <Route index element={<Bookings />} />
  <Route path="create" element={<CreateBooking />} />
  <Route path=":id" element={<ViewBooking />} />
  <Route path=":id/edit" element={<EditBooking />} />
  <Route path=":id/delete" element={<DeleteBooking />} />
</Route>

Layouts

Layouts are no pages at all, they are more like components, so they can be treated like that, but lately, I prefer to put them into a layout folder, it makes more clear that in this app there are n layouts available.

layout
├── main.tsx
└── auth.tsx

One thing you may notice is that I don’t call them main-layout.tsx but just main, that’s because following this reason I would have to rename all the components like table-component.tsx which is weird. So I name all the components without the obvious suffix given by the parent directory, and if I need to underline that I’m using a layout I can always use an import alias as well.

import { Main as MainLayout } from "@/layouts/main.tsx";

Contexts, Hooks & Stores

This is pretty simple, and usually, I see almost every developer stick with something like this, so I’m gonna put here how I organize those things:

hooks
├── use-users.ts
└── use-click-outside.ts
contexts
├── workbench.tsx
└── authentication.tsx

Here again, I stick with using kebab-case for all the filenames, so I don’t have to worry about which ones are capitalized and which are not. For the testing files, due to the fact, that the custom hooks are few, I would not create a specific folder, but in my opinion, if you want to be very strict. you can do it as well:

hooks
├── use-users
   ├── use-users.ts
   ├── use-users.spec.ts
   └── index.ts
└── use-click-outside.ts

Helpers

How many times do you create a nice formatCurrency function without knowing where to put it? The helpers folder is coming to your help!

Usually, here I put all the files I use to make code look better, I don’t care if the function is used more than one time or not. Usually, these helpers are fairly few, so until there is a very large number of them, I stick this way.

helpers
├── format-currency.ts
├── uc-first.ts
└── pluck.ts

Constants

I see a lot of projects that contain contansts in the utils or helpers folder, I prefer to put them into a specific file, giving the user a nice view of what is used as a constant in the app. Most of the time I put only globally scoped constants, so don’t put the QUERY_LIMIT constant here if it is used in only one function for a very specific case.

constants
└── index.ts

Also, I usually keep all the constants in a single file. It is no sense to split every constant into a specific file.

// @/constants/index.ts
export const LINKEDIN_FULLNAME = "Renato Pozzi";
export const TWITTER_USERNAME = "@itsrennyman";

// And use them in your app! 👍
import { LINKEDIN_FULLNAME, TWITTER_USERNAME } from "@/constants";

Styles

Simply put global styles into a styles folder, and your game is done.

styles
├── index.css
├── colors.css
└── typography.css

What about CSS for my components?

Good question mate! Do you remember the component folder we talked about a little while ago? Well, you can add more files depending on your needings!

button
├── button.tsx
├── button.stories.tsx
├── button.styled.tsx
├── button.module.scss
├── button.spec.tsx
└── index.ts

If you are using emotionstyled-components, or simply the CSS Modules, put them into the specific component folder, so everything will be optimally packaged.

Config Files

Does your application have configuration files, like Dockerfiles, Fargate Task Definitions, and so on? The config folder should be the perfect place for them. Putting them into a proper directory avoids root directory pollution with non-relevant files.

APIs

99% of the react application have at least one API call to an external endpoint (your backend, or some public service), usually these operations are performed in a few lines of code without too much difficulty, and that is why in my opinion an optimal organization is underestimated.

Consider this piece of code:

axios
  .get("https://api.service.com/bookings")
  .then((res) => setBookings(res.data))
  .catch((err) => setError(err.message));

Quite simple right? Now imagine you have these 3 lines spreaded across 10 components because you use a lot of time this particular endpoint.

I hope you don’t want to do a search and replace for all the URLs in the app, furthermore, if you are using TypeScript, import every time the response type it’s quite repetitive.

Consider instead using an api directory, which first of all contains a consistent instance of the client used for the calls, like fetch, or axios, and also the files keeping the fetch calls declarations inside!

api
├── client.ts
├── users.ts
└── bookings.ts

And an example of the users.ts file:

export type User = {
  id: string;
  firstName: string;
  lastName: string;
  email: string;
};

export const fetchUsers = () => {
  return client.get<User[]>("/users", {
    baseURL: "https://api.service.com/v3/",
  });
};

Wrapping Up

It’s been a long road, and I hope the information in this article is useful to you as you build your new and existing projects. There is still a lot to say, there are always special cases to take into consideration, but the points covered in this article are the most used by all react developers.

Do you also use one or more of these techniques in your projects? Let me know via Twitter or LinkedIn!