How React Helped Us Scale a Large Web App

Andrew Clark
OpenGov Developers
4 min readJun 1, 2016

--

“Scaling” a web app usually refers to maintaining performance as the number of users or requests increases. In this post, we’ll discuss a different kind of scaling: maintaining quality as the product’s breadth increases, and as more engineers are added to the team.

At OpenGov, our product consists of many apps that work together to provide a consistent experience. As is often the case in start-ups, many of these apps were originally developed in parallel, over slightly different timescales by different teams of engineers, and using a variety of JavaScript libraries.

But as our engineering team has matured, we’ve consolidated our efforts around a single, unified architecture based on React. Much of this work has involved migrating apps written in legacy frameworks to our new stack. Over the last year, we’ve also had experience writing apps in React from the ground up.

Along the way, we’ve had to figure out how to write and maintain large React applications that scale — not only to more users, but to more product features and engineers contributing to our codebase. Here’s some of what we’ve learned:

React helps us wrangle technical debt

At a high level, React’s component model is radically simple: components receive props and output other components. This encourages you to think about your app in terms of functional units, and makes the boundaries between components more important than their actual implementation.

As Ryan Florence puts it:

While we do our best at OpenGov to avoid “garbage” code, the broader point is one we agree with.

A common workflow for developing new product features with React begins by scaffolding the UI in the most naïve way possible, experimenting quickly to find the best component structure, getting to a minimally sufficient implementation, then wrapping everything up through iterative improvement. It’s a process that fits naturally within an agile team.

This is good engineering practice regardless of the library or framework you’re using, but it’s especially easy with React. Like functions, components are all about inputs and outputs. If a component works in one part of an app, you can be fairly certain it will work anywhere else, given the same set of props. Because React components are so well-encapsulated, you have the freedom to refactor individual components without worrying about breaking a different part of the app. For the same reason, technical debt metastasizes at a slower rate, because “garbage” code can be confined to self-contained components.

React has strong opinions (and escape hatches)

While React has a smaller, more focused API than other frameworks, it’s embedded with strong opinions that encourage good coding practices: declarative over imperative code, immutable over mutable data, and derived data over duplicated state. For example, layout thrashing isn’t something the average React developer needs to think about, because React abstracts away DOM updates entirely.

But for those exceptional cases where direct access to the DOM is needed, lifecycle hooks and refs can be used to manipulate the underlying node of a component. We use this feature to integrate external libraries (like D3) with React’s component model. These “escape hatches” are a crucial part of React’s success at building real-world, scalable applications.

That’s not to say we haven’t encountered problems. Some of React’s supposed virtues cut both ways.

It’s often remarked that React has a small API surface area. This is mostly a blessing, but like any design decision, it comes with some trade-offs. Compared to JavaScript frameworks like Ember and Angular, React has little in the way of conventions for even the most basic tasks.

An unavoidable truth about the React ecosystem is that React alone does not offer a complete architecture for developing front-end applications.

There are many community resources and third-party libraries that aim to fill in the gaps, but the array of options can be overwhelming. Some people will tell you to use component state for almost everything; others encourage you to externalize state into Flux stores. Some do data fetching inside lifecycle hooks; others do this at the route level. Until recently, choosing a Flux framework was an exhausting exercise, and while most of the community has settled on Redux, even within the Redux community, consensus can be elusive.

React alone isn’t enough

One problem we suffered from when we first started using Redux was an over-eagerness to extract state into our Redux store. In some cases, this led to using Redux in places where component state was a better fit.

Problems can arise when a component unmounts but its “state” is left over inside the store, or when two different components are attempting to read and modify the same slice of state. It also involves splitting logic over multiple files. By contrast, component state has the enormous benefit of being confined to a single component. Redux works best for concerns that are more global — or app level — in nature.

Once we realized our error, we found it was harder to refactor Redux reducers back to normal component state than it would have been to refactor component state into a Redux reducer.

Now, our approach is to implement features using component state first, and only move things into Redux once it becomes necessary.

--

--