Custom React Stack
React has a very rich ecosystem. For anything you want to do, there is probably a library or a framework available for it. That's great, but having too many options can also be very confusing. Do you want to start out simple or go for the ultimate because your app demands it? What's the right set of options for your specific use case?
This guide will help you build your own custom React stack, explaining key options and tradeoffs at each step.
Another advantage of this approach is that you will know exactly what's in your stack. When that new shiny technology comes along, you will be in a better position to slot it in.
Our Sample App: Movie Magic
For the purpose of this discussion, imagine that you want to write a movie streaming app - Movie Magic. The app should present the available movie titles and help users make a choice. It should also allow them to manage their subscriptions.
Here's a very humble beginning, just two pages:
- A Home page showing the list of top 10 movies:
- A Settings page for users to manage their subscription:
Click here to test drive the final application. As you can see, it doesn't do much yet. However, this is good enough for the purpose of our discussion. Let's start by discussing our architecture choices.
Tech Stack Options
The diagram below shows the key items that make up our tech stack, along with some options. Subsequent sections will discuss the pros & cons of each option. Note that the diagram is meant to be read bottom up - imagine that we are building a stack.
Monorepo vs. Multi-repo
For a really simple app, you can just create a single Git repo and call it a day. But what if you want to separate out reusable components into a library? Well, you can create a separate repo for it and put the compiled output in a binary repository (such as npm or Artifactory). See below. The app can pick up the library from the binary repository by adding a dependency to it. This is called a multi-repo set up (two repos in this case).
Continuing this scenario, what happens when you have multiple applications and multiple libraries with complex dependencies on each other? Let's say we start adding a new repo for each application and each library. This is now starting to look ugly.
Here are some issues with the multi-repo approach:
- It is cumbersome to add new repos. You have to set up new tooling, new CI/CD pipelines, add committers, and the list goes on.
- You start to see duplicate code in your repos because people are reluctant to put in the effort to create a new repo just to reuse code. It is much easier to copy code from another repo.
- It is difficult to maintain consistent tooling across repos.
A solution that works better in such use cases is a monorepo.
Monorepo is a single repository containing multiple distinct projects, with well-defined relationships.
Here's an example of taking the multiple repos in the above diagram and replacing them with a single repo:
Here are the advantages of a monorepo:
- No overhead to create new projects
- Easy to refactor code for reuse
- Consistent way of building every project
- Bug fixes are available immediately to all dependent projects and can be tested exhaustively before committing
- Developers can confidently contribute to any project
You can build a monorepo manually by putting all your projects into one repo and figuring out a way to organize and build them. However, people generally use an off-the-shelf monorepo platform such as Lerna, Yarn Workspaces, npm Workspaces, Turborepo or Nx.
Turborepo and Nx provide some advanced features:
- Caching of build artifacts for faster builds
- Parallelize builds based on dependencies
- Build only the affected projects
- Visualize dependency graph (this helps in avoiding cyclic dependencies)
Turborepo is my favorite because it is less opinionated - it builds on top of npm or Yarn Workspaces. This approach allows me to integrate existing templates more easily into my monorepo. Each project has its own
package.json file, making it easy to track why a particular dependency was added.
Nx, on the other hand, keeps the dependencies for all projects in a single giant
package.json sitting at the root. This helps maintain a single version policy. However, this has its own pros & cons (you can google for opinions). Note that Nx can be used in a bare-bones way without using Nx plugins. In this mode, Nx simply builds on top of npm or Yarn Workspaces and becomes equivalent to Turborepo.
CSR vs. SSR
Client-Side Rendering (CSR)
Server-Side Rendering (SSR)
Next.js and Remix are a two of the most popular Server-Side Rendering frameworks in the React ecosystem.
Movie Magic Tech Stack
Given the options discussed above, I decided to build Movie Magic using three different stacks to illustrate the differences: Classic React, Next.js & Remix. The chart below shows the decisions made within each stack.
Movie Magic Repo Structure
Building Movie Magic
npm install npm run dev
Open browser windows at each of the following URLs to see the respective demo apps:
- http://localhost:3000/: Movie Magic | React
- http://localhost:3001/: Movie Magic | Next.js
- http://localhost:3002/: Movie Magic | Remix
Note that the React app fetches mock data from MSW, whereas the other two apps fetch real data from the movie-magic-api.
Note: Do not run
npm installin any of the subdirectories. It will break the build. There should be only one
package-lock.jsonfile in the entire repo (at the root).
To build all apps and packages, run the following command:
npm install npm run build
Removes all build artifacts and performs a clean build.
npm run clean npm install npm run dev
For an "aggressive" clean build, add one more step as shown below. This wil build the lock file from scratch.
npm run clean rm package-lock.json npm install npm run dev
cd storybook npm install npm run storybook # you can also run it from the root directory
Running Unit Tests
npm run test
Running End-to-End Tests
npm run dev # starts a local server hosting the react app # run cypress in a different shell npm run cypress
npm run format