In early 2015, Airtable had just launched publicly and released its iOS app. After a long product sprint in preparation for the launch and release, our engineering team finally had the opportunity to take a step back and try out new frameworks and tools as a foundation for our next phases of work.
As part of that effort, that summer I decided to explore the possibility of using React in Airtable’s web client. I had experience using React both during my time as an engineer at Facebook and in a few personal projects. This experience gave me familiarity with the design philosophies behind React, and empowered me to advocate for it at Airtable.
Airtable’s web client codebase at the time consisted largely of two types of classes: model classes, which keep data in sync in realtime between web clients and Airtable’s servers, and component classes, which render model-based data and other data to the DOM. As a library for building user interfaces, React would take the place of our existing components; meanwhile, we could continue to use existing model code for storage, state changes, and permission checks. This consideration made React preferable to a more opinionated full-stack framework which would require adapting or replacing our model layer.
Another important consideration was that React can gracefully coexist alongside non-React component code. React has built-in escape hatches that gave us the flexibility we needed to combine React code with existing UI code in a number of different ways, making gradual adoption possible.
From my experience with React, I expected that adopting React would benefit us in three ways:
- Reduced boilerplate: We could write simpler UI code when working with a virtual DOM. While using React, an Airtable engineer could write a
render()function for each component that would provide a mapping from state to DOM markup or child components, and React's virtual DOM system would determine what actual DOM mutations to perform when a component was mounted or when its state changed. Our existing code before React had to manually describe the specific DOM mutations required by numerous different state changes. The switch to React would obviate the need to write this tedious code and eliminate all the potential bugs that might result, by letting us write code at a higher level of abstraction.
- Decomposition: We could more effectively organize and decompose our UI code. Prior to React, each of our components needed a
.jsfile to describe DOM mutations and event handling, and a separate HTML-like
- Security: We could write code that was more secure by default. React’s JSX markup cleanly distinguishes HTML tags from other strings and escapes the latter automatically. In our previous framework, some types of DOM mutations could introduce XSS vulnerabilities if we weren’t careful to escape user-supplied strings.
With the above considerations in mind, I went ahead with a pilot project. I rewrote our view configuration menus in React, which include our filtering and sorting menus. After those changes were written, tested, and shipped, and we saw the above benefits, we decided to adopt React more widely across our codebase.
Prioritizing what to rewrite in React
Over the next few months, we prioritized rewriting features in React as follows:
- We prioritized rewriting code that would greatly benefit from React’s convention of one-way data flow. Our field configuration menu was a good candidate, as it has to maintain its own temporary state to store any unsaved field configuration, rather than relying on a model class for storage. Its state had been spread out across many different component files over time, and was increasingly difficult to understand. The use of one-way data flow guided us to re-architect this menu in way that we found more readable and decomposable.
- We prioritized rewriting parts of our product where lots of new features were planned. Rather than adding features to the pre-React version of a component, we usually preferred to first rewrite the component in React and then add new features on top of the React rewrite. We found that the latter approach has been easier to code review and easier to maintain in the long run, making it worthwhile even taking into account the one-time cost of the rewrite.
- We prioritized creating a library of small, reusable components that show up across our product, like buttons, menus, and icons. Centralizing this code into library components ensures a consistent look-and-feel across our product and makes it easy to update for site-wide design refreshes. It also provides a useful set of building blocks to streamline future feature work.
We avoided rewriting features just for the sake of rewriting them, if they didn’t fall into any of the above categories. In particular, we kept around some Grid View components that were highly optimized for rendering performance and unlikely to work as performantly in React without duplicating a lot of effort.
Managing mixed React & non-React code
Even a year into our adoption of React, there are still many parts of our codebase where we mix React code with our previous framework, with low-level DOM calls, or with other libraries. As more of my teammates have ramped up on React, it’s been essential for all of us to understand and take advantage of React’s interoperability features.
Since Airtable was originally built in an internal framework that touches the DOM directly, we’re often creating React DOM elements as children of existing non-React DOM elements. The React top-level API allows us to do this easily, and there are currently dozens of
ReactDOM.render() calls in our codebase.
In addition, a few of our React elements have child DOM elements that are managed directly by our code rather than by React. This comes into play when we convert a component to React without converting all of its subcomponents. For example, our mostly React-based field configuration menu has a non-React based formula editor as a child. For this, React's lifecycle methods such as
componentDidMount() have been useful as an entry point to mount and unmount code that uses other libraries.
Familiarity with React's escape hatches is also useful in situations that require more fine-grained control over the DOM than React can provide. Our tabbed table navigation at the top of the page takes a hybrid approach, using React for rendering tab names and menus, while using low-level DOM measurement and styling to lay out the rows of tabs bottom-to-top and ensuring the small plus-sign button to create a new table isn't orphaned on its own row.
Looking back one year later
By reducing the time we spend writing and testing low-level DOM code, React has freed up our engineers’ bandwidth to focus on the user interface challenges that we love: building the best product to help people organize anything.