React is an open source JavaScript library for building user interfaces created and maintained by Meta (Facebook).
It is probably the most popular library for building user interfaces nowadays. The reason for its popularity is that it is quite performant and has a small API, which makes it a simple yet very powerful tool for creating user interfaces.
It is component-based, which allows us to split large applications into smaller parts and work on them in isolation.
React is also great because its core API is decoupled from the platform, allowing projects such as React Native to exist outside the web platform.
One of React’s biggest strengths but also weaknesses is that it is very flexible. This has allowed its community to build great solutions. However, defining a good application architecture out of the box can be challenging.
Making the right architectural decisions is crucial for any application to succeed, especially once it needs changes or it grows in terms of size, the number of users, and the number of people working on it.
In this chapter, we will cover the following topics:
By the end of this chapter, we will learn to think a bit more from the architectural point of view when starting React application development.
Every application uses some architecture, even without thinking about it. It might have been chosen randomly and might not be the right one for its needs and requirements, but still, every application does have an architecture.
That’s why being mindful of the proper architecture at the beginning is essential for every project. Let’s define a couple of reasons why:
It is worth noting that all applications are prone to requirement changes, so it is not always possible to predict everything upfront. However, we should be mindful of the architecture from the start. We will discuss these reasons in detail in the following sections.
Every building should be built on solid foundations to remain resilient to different conditions such as age, weather conditions, earthquakes, and other causes.
The same thing can apply to applications. Multiple factors cause various changes during a project’s lifetime, such as changes in requirements, organization, technologies, market, finance, and more. Being built on solid foundations will make it resilient to all those changes.
Having different components organized properly will make organizing and delegating tasks much easier, especially if a larger team is involved.
Good component decoupling will allow better splitting of work between teams and team members and faster iterations without team members being blocked by each other.
It also allows better estimates to be made regarding how much time is required for a feature to be completed.
Having a good architecture defined allows developers to focus on the product they are building without overthinking the technical implementations since most of the technical decisions should have already been made.
Besides that, it will provide a smoother onboarding process for new developers, who can be productive quickly after familiarizing themselves with the overall architecture.
All the reasons mentioned in the previous sections indicate that the improvements a good architecture brings will reduce costs.
In most cases, the most expensive cost of every project is people and their work and time. Therefore, by allowing them to be more efficient, we can reduce some redundant costs a bad architecture could bring.
It will also allow better financial analysis and planning of pricing models for software products. It will make it easier to predict all the costs the platform requires to be functional.
Making all team members productive gives them time to focus and spend more time on important things, such as the business requirements and the needs of users, rather than spending most of the time fixing bugs and reducing technical debt.
Better product quality will also make our users more satisfied, which should be the end goal.
To exist, every piece of software needs to meet its requirements. We’ll see what these software requirements are in the following section.
In this section, we will focus on React and see what things are necessary to consider when building React applications and the main challenges most React developers face when building their applications.
React is a great tool for building user interfaces. However, there are some challenging things we should think about when building an application. It is very flexible, which is both a good and a bad thing. It is good in the sense that we can define the architecture of different parts of the application without the library getting in our way.
By being so flexible, React has gathered a large community of developers worldwide, building different open-source solutions. There is a complete solution for literally any problem we might encounter during development. This makes the React ecosystem very rich.
However, that flexibility and ecosystem richness come with a cost.
Let’s take a look at the following React ecosystem overview diagram made by roadmap.sh:
Figure 1.1 – React developer roadmap by roadmap.sh
As shown in Figure 1.1, there is a lot to consider when building an application with React. Let’s also keep in mind that this diagram might show just the tip of the iceberg. Many different packages and solutions could be used to build the same application.
Some of the most frequent questions when starting with a new React application are as follows:
These challenges are not limited to React – they are relevant to building frontend applications in general, regardless of which tools are being used. But since this book focuses on React, we will be approaching them from that perspective.
Because React is very flexible and has a very small API, it is unopinionated about how we should structure our projects. Here is what Dan Abramov, one of the maintainers of React, says on this:
And that is a very good point. It will mostly depend on the nature of the application. For example, we wouldn’t organize a social network application and a text editor application in the same way because they have different needs and different problems to solve.
It depends on the nature of our application.
If we are building an internal dashboard application, a single-page application is more than enough.
On the other hand, if we build a customer-facing application that should also be public and SEO-friendly, we should think about server-side rendering or static generation, depending on how often the data on the pages are being updated.
React comes with built-in state management mechanisms with its hooks and Context API, but for more complex applications, we often reach for external solutions such as Redux, MobX, Zustand, Recoil, and others.
Choosing the right state management solution is something that depends a lot on the application’s needs and requirements. We will not reach for the same tools if we are building a to-do app or an e-commerce application.
It mostly depends on the amount of state that needs to be shared across the entire application and the frequency of updating those pieces of state.
Will our application have a lot of frequent updates? If that is the case, we might consider atom-based solutions such as Recoil or Jotai.
If our application requires a lot of different components to share the same state, then Redux with Redux Toolkit is a good option.
On the other hand, if we do not have a lot of global states and don’t update it very often, then Zustand or React Context API, in combination with hooks, are good choices.
At the end of the day, it all depends on the application’s needs and the nature of the problem we are trying to solve.
This one mostly depends on preference. Some people prefer vanilla CSS, some people love utility-first CSS libraries such as Tailwind, and some developers can’t live without CSS in JS.
Making this decision should also depend on whether our application will be re-rendered very often. If that is the case, we might consider build-time solutions such as vanilla CSS, SCSS, Tailwind, and others. Otherwise, we can use runtime styling solutions such as Styled Components, Emotion, and more.
We should also keep in mind whether we want to use a pre-built component library or if we want to build everything from scratch.
This depends on the API implementation. Are we using token-based authentication? Does our API server support cookie-based authentication? It is considered to be safer to use cookie-based authentication with httpOnly cookies to prevent cross-site scripting (XSS) attacks.
Most of these things should be defined together with the backend teams.
This depends on the team structure, so if we have QA engineers available, we will be able to let them do end-to-end tests.
It also depends on how much time we can devote to testing and other aspects. Keep in mind that we should always consider having some level of testing, at least integration, and end-to-end testing for the most critical parts of our application.
Regardless of the specific needs of the application, there are some generally bad and good decisions we can make when building it.
Let’s look at some of the bad architectural decisions that might slow us down.
Imagine having a lot of components, all living in the same folder. The simplest thing to do is to place all the React components within the components folder, which is fine if our components count does not exceed 20 components. After that, it becomes difficult to find where a component should belong because they are all mixed.
Having large and coupled components have a couple of downsides. They are difficult to test in isolation, they are difficult to reuse, and they may also have performance issues in some cases because the component would need to be re-rendered entirely instead of us re-rendering just a small part of it that needs to be re-rendered.
Having a global state is fine, and often required. But keeping too many things in a global state can be a bad idea. It might affect performance, but also maintainability because it makes it difficult to understand the scope of the state.
The number of choices in the React ecosystem makes it easier to choose the wrong tools to solve a problem – for example, caching server responses in the global store. It may be possible, and we have been doing this in the past, but that doesn’t mean we should keep doing that because there are tools to solve this problem, such as React Query, SWR, Apollo Client, and so on.
This is something that shouldn’t ever happen, but it is still worth mentioning. Nothing is preventing us from creating a complete application in a single file. It could be thousands of lines long – that is, a single component that would do everything. But for the same reason as having large components, it should be avoided.
Many hackers on the web are trying to steal our user’s data. Therefore, we should do everything possible to prevent such things from happening. By sanitizing user inputs, we can prevent hackers from executing some malicious piece of code in our application and stealing user data. For example, we should prevent our users from inputting anything that could be executed in our application by removing any parts of the input that might be risky.
Using unoptimized infrastructure to serve our application will make our application slow when accessed from different parts of the world.
Now that we have covered some bad architectural decisions, let’s see how to improve them.
Let’s look at some of the good decisions we can make to make our application better.
Splitting the application structure into different features or domain-specific modules, each responsible for its own role, will allow better separation of concerns of different application pieces, better modularity of different parts of the application, better flexibility, and scalability.
Instead of putting everything in a global state, we should start by defining a piece of a state as close as possible to where it is being used in the component and lift it only if necessary.
Having smaller components will make them more testable, easier to track changes, and easier to work in larger teams.
Have each component do as little as possible. This makes components easy to understand, test, modify, and even reuse.
Relying on static code analysis tools such as ESLint, Prettier, and TypeScript will improve our code quality without us having to think too much about it. We just need to configure these tools, and they will let us know when something is wrong with our code. These tools also introduce consistency in the code base regarding formatting, code practices, and documentation.
Having users worldwide means our application should be functional and accessible from all over the world. By deploying the application on a CDN, users all over the world can access the application in the most optimal way.
Now, let’s apply the principles we just learned about to a real-world scenario where we will be planning the application that we will be building.
We will be building an application that allows organizations to manage their job boards. The organization admins can create job postings for their organizations, and the candidates can apply for the jobs.
We will be building an MVP version of the application with the minimum set of features, but it should be extendable for more features in the future. At the end of this book, we will cover the features that the final application could have, but to keep things simple, we will be focusing on the MVP version.
Proper application planning starts with gathering the requirements of the application.
The application has two types of application requirements:
Functional requirements should define what the application should do. They are descriptions of all the features and functionalities of an application that our users would use.
Our application can be split into two parts:
Non-functional requirements should define how the application should work from the technical side:
To better understand how our application will work under the hood, it is helpful to understand its data model, so we will dive into that in this section.
In the following diagram, we can see what our data model looks like from the database perspective:
Figure 1.2 – Data model overview
As seen in Figure 1.2, there are three main models in the application:
Defining the application requirements and data model should give us a good understanding of what we are building. Now, let’s explore the technical decisions for our application.
Let’s see what technical decisions we need to make for our application.
We will be using a feature-based project structure that allows good feature isolation and good communication between the features.
This means we will create a feature folder for every larger functionality, which will make the application structure more scalable.
It will scale very well when the number of features increases because we only need to worry about a specific feature and not the entire application at once, where the code is scattered all over the place.
We will see the project structure definition in action in the upcoming chapters.
When it comes to the rendering strategy, we are referring to the way the pages of our application are being created.
Let’s look at the different types of rendering strategies:
Since our application requires multiple rendering strategies, we will use Next.js, which supports each of them very well.
State management is probably one of the most discussed topics in the React ecosystem. It is very fragmented, meaning there are so many libraries that handle state that it makes it difficult for the developers to make a choice.
To make state management easier for us, we need to understand that there are multiple types of states:
Styling is also a big topic in the React ecosystem. There are many great libraries for styling React components.
To style our application, we will use the Chakra UI component library, which uses Emotion under the hood, and it comes with a variety of nice-looking and accessible components that are very flexible and easy to modify.
The reason for choosing Chakra UI is that it has a great developer experience. It is very customizable, and its components are accessibility-friendly out of the box.
The authentication of our application will be cookie-based, meaning that on a successful auth request, a cookie will be attached to the headers, which will handle user authentication on the server. We are choosing cookie-based authentication because it is more secure.
Testing is a very important method of asserting that our application is working as it’s supposed to.
We don’t want to ship our product with bugs in it. Also, manual testing takes more time and effort to discover new bugs, so we want to have automated tests for our application.
There are multiple types of tests:
This was an overview of how our application should work. Now, we should be able to start implementing it in code in the upcoming chapters.
React is a very popular library for building user interfaces, and it leaves most of the architectural choices to developers, which can be challenging.
In this chapter, we learned that some of the benefits of setting up a good architecture are a good project foundation, easier project management, increased productivity, cost-effectiveness, and better product quality.
We also learned about the challenges to consider, such as project structure, rendering strategies, state management, styling, authentication, testing, and others.
Then, we covered the planning phase of the application that we will be building, which is an application for managing job boards and job applications by gathering requirements. We did that by defining the data model of the application and choosing the right tools to overcome the architectural challenges.
This has given us a good foundation to implement our architecture in a real-world scenario, as we will see in the following chapters.
In the next chapter, we will cover the entire setup that we will be using for building the application.