5 Tips To Scale Up Your React Apps

React best practices that work for production just as well as they do for personal projects.

Prithwish Nath
JavaScript in Plain English

--

Photo by Our Life In Pixels @ Unsplash

Scalability is not a KPI that you can specifically target. It’s a function of good design decisions and maintainable code. Unfortunately, this equation isn’t in favor of developers. The bigger the project, the stronger the temptation to take shortcuts and “just do it this way for now, get it out the door quickly, we can come back and fix this later”.

Software engineering is difficult enough, please don’t make it harder on yourself.

Work smarter instead, with these five tips that will save your bacon in any environment — whether you’re: a solo dev, working on personal projects, a chaotic “we need rockstar developers!” startup, or a company with multiple disparate teams working on a monorepo.

Spoiler: “Use TypeScript!” isn’t included here, but if you value your sanity, consider that the default rule for when you’re ready to move past ToDo apps.

1. Use Bit to Take Full Advantage of React Component Composability

Thankfully, React’s core principle is also its easiest to understand: composability. You build JavaScript functions that manage relationships between UI and state and produce JSX/TSX as output, and you use these independent, reusable pieces to build progressively bigger UI elements as you scale your app.

To make it even simpler: Build atoms → Use atoms to build molecules → Decide where molecules fit in your app → Rinse and repeat as necessary.

That’s where Bit comes in. Bit is an open-source toolchain that lets you double down on composability, so you can build, store, test, and document components in isolation, then store them in public, private, or unlisted repositories with automatic versioning.

“Is this another UI library?”

Not at all!

Bit is a way to do all of the above with anything in React that is composable. You, your team, or anyone in the world can discover and share independent atoms and molecules on Bit — styled or headless; components, utility functions, custom hooks, or themes — and use them in their own apps, in their own context, all with a simple npm install.

Need a Modal for your auth page? Get one.

2. Use a Design System

Quick question: if your websites/web apps will now be seen by thousands of users instead of hundreds, which do you need more: the flashiest CSS and slickest animations, or a design that is maintainable, can be iterated upon incrementally, and is easily debuggable?

Trick question. These two goals are not mutually exclusive.

You can deliver the fanciest designs possible, while still structuring them in a way that makes your developers happy. How? Enter design tokens.

Here’s what design tokens exported by your design team could look like.

Design tokens are colors, margins, padding, line heights & spacing, font families, transitions, keyframes — anything that sees repeated use. You can get these values from your design team, and consolidate them in a decoupled layer that is JSON, YAML, or even just JavaScript objects, that sit on top of your apps and can be used by your dev team to provide consistent, coherent, extendable UI elements throughout your app.

If you’re using Bit, theming with design tokens becomes even easier. You can just grab one of the many theming components there that use a Provider pattern (Team Bit’s own open-sourced ThemeProvider is good enough) to build a Context-aware theme using only design tokens as input, provide it to all-consuming components by wrapping them within the Provider, et voila, you have an app-wide theming system where themes are hot-swappable.

3. Write Custom Hooks for Managing Your Network Requests

This way you’ll abstract away async boilerplate, request bodies, and options/configs behind a custom hook, and instead, focus on building your UI + managing application states.

Yes, even if they’re only a wrapper around your axios/fetch calls.

If you’re using a library like SWR or react-query, do the exact same thing still, this time per query.

The approach should be to refactor each query into its own custom hook (usePosts, usePostsByUser, useUserById, and so on). This design makes it much easier to manage query keys and shared query logic.

This makes your network requests modular, making sure it would be a drop-in replacement if you ever decide to change HTTP libraries (from simple axios/fetch all the way to react-query, RTK, and such).

💡 Even better, this way you can share them to Bit so others can use your hooks via the package manager of their choice.

4. Don’t Manage State You Shouldn’t Be Managing

To understand this point, ask yourself what React ‘state’ even is. Just data that describes the current condition of a system, right? This implies there can be, broadly speaking, two different kinds of state.

  1. Data that you create and generate yourself to describe the internals of your app — a boolean value that keeps track of whether a modal or sidebar is open, objects for unsaved form data, etc.
  2. Data that you fetch asynchronously via an API (there’s more, but let’s limit ourselves to this definition for now), that describes the internal state of the server, or database that you’re fetching it from. You’re only borrowing this data, and might not even be seeing the most recent version of it as it might have been changed by others in the meantime.

It’s only the first kind of state should you manage and let your users interact with for synchronous UI operations.

Example scenario: Fetching a list of Todos from your server, and letting your user update/delete/mark them as complete in the UI. What you should not be doing is managing the fetched data with GET/POST requests to the server on every single operation, that would be incredibly slow and introduce all the errors and unintended behavior.

The right approach, then, would be to initialize the application state with server data, and only operate on the former, syncing it with the server on an interval or on an event trigger.

Now you know what happens when you’re writing an article on Medium! Unsaved changes exist only on the frontend, and periodically get saved to the server.

💡 For managing client state, you want to use React’s useState/useReducer hooks at a component level, and the Context API (pro tip: stick to using this for values that are mostly static) or a third-party state management library like Zustand or Redux on an app level. For server state/cache management, use libraries like SWR, apollo, or react-query.

5. A Streamlined Project Structure Is Key

React is unopinionated; its biggest strength and weakness in one. There are plenty of opinions and ways of organizing React projects, some of which conflict. But everyone can agree on one thing— regardless of project scale, you need a structure that has the following:

  1. A file/folder structure that makes your codebase immediately skimmable to anyone jumping in to look at the code, whether they’re new hires or experienced contributors. One approach is to organize components by feature vs. just dumping a bunch of them in a shared ‘components’ directory.
  2. Code conventions that are obvious and intuitive enough so anyone working on your project won’t have to read a guideline to even get started. This is why TypeScript makes it so easy to work on software in teams, for example. The code itself becomes your documentation.
  3. Codebase structure that is easy to understand and build locally during development. This is why the multi-package monorepo architecture is a great starting point (if your API isn’t public) so you have all of the above plus shared types, shared libraries, and the advantage of one pull request per feature. Plus, with each feature abstracted away into its own independent thing and package, your imports become more understandable at a glance : import { Component } from ‘@project/ui/thing’ instead of import { Component } from ‘../../../components/thing’.
  4. Code that is optimized for refactoring. Don’t strive for perfect code that never ever fails. Instead, build better safety nets for when it does fail. Write code that doesn’t couple dependencies and logic so tightly that it basically nails them down to the floor, making even the thought of a refactor the stuff of nightmares.

💡 This is another area where Bit makes sense, with its focus on independently stored, documented, and tested building blocks that are semver’d into independent packages that can be stashed away into your organization’s namespaced repository, and imported into whatever project, whenever required, by whomever.

“Perfect is the Enemy of Good”

Today, we’ve talked a lot about pitfalls that you’ll face when building projects to scale, but the TL;DR as you grow your app, is simply this:

Stop thinking of projects as tightly-coupled monoliths and instead, think of them as a combination of independent building blocks that have their own feature set, that you can then architect together in a way that is intuitive and easily modifiable. This is the kind of thinking that Bit promotes, turning web dev into playing with Lego.

Scalability will follow from there, organically. You won’t have to think about it.

In Plain English 🚀

Thank you for being a part of the In Plain English community! Before you go:

--

--