React and Recoil

Project requirements

In order to achieve both performance and user experience objectives for Alexandria, we settled on a number of goals to fullfil at the initial stages.

One of the key features of the app is to reflect the changes made by a particular user interaction immediately, without an additional page reload.

Consider the following user actions and their effects on a piece of text. Our challenge is to make every user interaction feel instantaneous and seamless.

User action Immediate Rendering
Click on word/phrase Translation input box available
Click on highlighted word/phrase Translation and status displayed
Update word/phrase status Highlighted colour on word changes

Another challenge is the ability of the UI to keep in sync with state changes of variables. With increased complexity, it becomes harder to track the exact state of the UI at every given moment during the lifecycle of the application. The challenge is to represent the UI accurately after a possible flurry of user interactions.

Plain JavaScript versus React

Pitting plain JavaScript and React against Alexandria’s initial requirements, React was the winner. Below is a summary of some of the differences between the two approaches pertaining to Alexandria.

Rendering efficiency

With both approaches, all interactivity is handled in the browser. Each user interaction resulting in a data change requires an API call to the back end, and a re-rendering of the parts of the page affected by the changes. For a plain JS solution we briefly considered a templating engine like Handlebars. While it is a good choice, it did not meet Alexandria’s goal of delivering speedy incremental updates of the DOM, modularity and robust state handling.

Instead we built our single page application using React: The app starts with a blank container and loads the UI. The UI is defined by a component that returns the JSX - a HTML lookalike. The new component is rendered into the div container using the ReactDOM library, and the result will appear directly on the browser.

Because each UI is broken into smaller components, each component can render independently due to its use of the virtual DOM. One change in a component does not result in the re-rendering of the entire page, merely the changed component, thus improving the front-end user experience significantly.

Tracking changes

With plain JavaScript and the DOM API, an event listener is attached to a DOM element to listen for changes. With each state change, we track the value deviation function, re-run the deviation functions and track the changes in return value. Those changed values are passed to the appropriate DOM element through the DOM API.

With React, the UI is set up to keep the entire state of a variable in the form of a “controlled component”, while an event (e.g. a button press) can be specified directly in the code, with no need for an event listener. The change to variable is registered, and the UI is updated automatically. There’s no need to go into the DOM to find the variable to update.

React’s implementation of hooks and component state fit perfectly with this requirement.

Reusability

In React, the app is split into components, where each component maintains the code required to both display and updates the UI. This also allows reusability of components, another benefit.

Managing state with Recoil

When it came to choosing a tool for global state management, we began by assessing what the application needed.

It was immediately clear to us that users will be interacting intensively with highly dynamic components: highlighted words, translations, status, and languages are all expected to change with every user interaction. Therefore, it was vital that after an update, each component state is accurately captured and rendered back to the users in the most efficient manner.

Turning our attention to state management tools available on the market, we quickly realized although the number of libraries and packages are numerous, most belong to one of three categories in addition to the React natively supported Context API: Flux (Redux, Zustand), Proxy (Mobx, Valtio), or Atomic (Recoil, Jotai) [3].

Within the context of Alexandria, we took a closer look at three choices: React’s Context API, Redux, and Recoil. Redux has long been the state management library of choice for applications of a certain size, while React’s very own Context API provided state management directly from React itself. Adding to the mix, new kid on the block Recoil had been released by Facebook, and touted as a React-specific state management library that maximizes developer happiness.

Choosing the right tool for the job took some careful examination. While we did not perform an exhaustive benchmarking study, some initial sandboxing helped us arrive at the following observations.

Context API

How it works

A context provider is created on the parent component where the global state sits. Data is passed down the tree to its children components. Those children components are able to access the context object from the parent through props drilling.

This works great when the context data changes only occasionally. But the solution breaks down when we store substantive amount of data in the context. In the case of Alexandria, the context data would change every time the user interacts with one word or phrase in the text, and causes unnecessary re-rendering of all components of the provider, in this case, all of the words/phrases.

Alternatively, the context object can be broken down into multiple states. This, however, introduces complexity into the codebase that could be avoided through another approach.

Pros

  • Context API is part of React’s built-in package, requiring no additional installation
  • Relatively easy to set up and start working
  • Great for static or rarely refreshed data

Cons:

  • Excessive unnecessary re-rendering affects performance

Redux

How it works

Instead of changing state by propagating values through the component tree, Redux takes a different approach. When a component state changes, the component dispatch an action to a reducer. The reducer handles the action (e.g. change a word state), manipulates the old state to reflect the new state. The reducer goes to the central store, which that manages all the states of the application. The application subscribes to part or all of the store, which then passes updated states as props to components that subscribe to it. This is how the application handles a state change.

Pros
  • Great for often-refreshed data, only listen to what has changed, only the value changed re-renders
  • Integrates with react-redux library
  • Server-side rendering possible
Cons
  • Much more complicated to get started, introduces substantial complexity to application

Recoil

How it works

Recoil was created to solve a number of specific challenges faced by large and/or interactive apps, where issues of persistence and robustness are central. More specifically, how does one keep different branches of the tree in sync where components live in different branches of the tree? How does one avoid re-rendering multiple components when state change(s) are detected in the same provider?

In order to meet these global state management challenges, Recoil implemented small, shared units of state named “atoms”, and derived states named “selectors”. Atoms are changeable and subscribable pieces of state. That is to say, one particular atom can be subscribed to by components, and atoms can be used to derive data from state. States are stored within the React tree, similar to how useState and useContext hooks function, making it easy to get started. Recoil encourages separation of state into separate pieces in order to route components. The existence of atoms is particularly well-suited for applications that receive a high frequency of updates.

Pros

  • Easy to set up and implement, experience of working with useRecoilState similar to working with useState
  • Re-renders only components with changed state, improves performance

Cons

  • Library still in beta

Choosing Recoil

Recoil became our state management library of choice based on two key considerations. One, it delivered optimal performance by rendering only component that changes upon user interaction. Two, its simplicity offered maximal developer happiness. The ease of set up and usage made state management a non-issue, where state changes are central to the viability of Alexandria.