An ongoing curiosity in JavaScript development communities is the question of how to build complex UI applications in React. This open source JavaScript library, maintained by Facebook, makes it easier than ever to write efficient, component-based projects in JavaScript. React has been around for about four years now, but the JavaScript community is still learning how to take full advantage of its powers.

The web UI for New Relic Mobile has several points of integration with components, services, and container objects built by different UI engineers across New Relic. Historically it’s been a challenge to cleanly integrate all the pieces. Additionally, we now have more non-frontend engineers than ever contributing to the UI, so it’s become very important for us to have a clean architecture that everyone can easily navigate and contribute to.

To make it easier for engineers who contribute code to the UI, we’ve created a flexible system, written in React. And since we’ve built this layered architecture from modules, we’ve been able to decouple the different domains of the UI application from each other, thus easing the burdens of contributors.

We wanted to share some of the design behind this complex React project because it’s led us to some seriously great outcomes.

react logo
Start monitoring your React data today.
Install the React quickstart Install the React quickstart

The new and improved New Relic Mobile UI architecture

Consider this diagram of the new architecture:

Mobile web UI React architecture

The top three layers are made up of our framework-specific UI views, containers, and components (which I’ll give a bit more detail for when I discuss the folder structure of our project). Where possible we favor composition over inheritance since React classes are great for this design pattern. For example, we use this pattern in the Handled exceptions sidebar container that has three child UI components: a search input, an occurrence count, and a bar chart component.

Handled exception sidebar

The sidebar composes these child UI components by passing needed properties to its children (for example, data to render the bar chart). This allows us to reuse these smaller components throughout the application and to handle layout and configuration through the larger container components.

We like to think of these UI components as “Lego pieces:” they can be used in multiple ways to build dozens of combinations. The UI views and containers, on the other hand, are much less modular and are more like jigsaw puzzle pieces in that they fit together only one specific way.

The bottom layer is composed of three collections of modules in which a significant portion of the Mobile UI application code base lives. These collections handle state management, asynchronous services, and store NRQL models (the latter of which powers the New Relic Query Language functionality in the UI).

Managing state

In React, state is an object that can change over time; for example, UI text changing to red in the case of an emergency is controlled by state. State can be managed—or held—in stores. In New Relic Mobile, some UI components manage their own internal state (dropdown components keep track of when they are open or closed), but other components, such as the filter list, filter picker, and time picker, use state containers (or bundles of state) that allow state to be easily shared across components.

To store the state React renders, we use the MobX library.

In the New Relic Mobile UI, the top-level stores that manage application state use singleton patterns. These singletons use object composition to reference the other store singletons in the project. For example, as shown in diagram below, the App store, which handles the UI’s high-level application state, has references to our Router store, Nav store, Time store, Filters store, and Account store.

Mobile UI state stores

So, for example, when a user clicks and “brushes” a graph (i.e., selects a data point in a graph that is then highlighted in another graph), we need to update our time window and route for the change. When the “brush” action is called, the App store mutates our router state and timestore state, which then triggers updates to all the components that observe those states. The API interface between our State store and the graph component is lightweight and simple, and this part of the UI is flexible if we ever require changes.

Running async services

This set of core modules are for a few asynchronous services that retrieve data from different places in the New Relic Mobile application. These store objects use the fetch API to make HTTP requests, and they manage the state of data as it loads.

These async store objects inherit from our HTTP request store (which is one of the few places where we use inheritance). The HTTP request store is a finite state machine that has some helper methods for handling loading, errors, and data. We chose inheritance for these objects because they all have the same HTTP request pattern (for example, fetch, is loading, has data, has error).

By moving that shared behavior to the HTTP request object and inheriting off of it, we removed hundreds of lines of redundant code from our project.

Plugged in NRQL models

The New Relic Query Language models are modules of code that generate NRQL queries used by the UI components, containers, and views. These models are pretty specific to the structure of the New Relic UI, so a close examination of them isn’t necessary here.

Bonus content: project folder structure

Finally, no discussion of a JavaScript project is complete without a look at the project’s folder structure. Obviously, there is no “perfect folder structure,” but this is what has worked well for our team in this project.

Application code lives in the src folder and styling lives in our scss folder. Both of these folders are bundled up by webpack and turned into UI assets. Our src folder has the following structure:

Mobile web UI React folder structure

The component folder holds React components that are given sets of properties and render widgets on the screen. These are our “Lego pieces” that can be composed into different combinations in the UI.

The containers folder is for components that compose other components. The containers are our puzzle pieces that fit only one way when used in the UI.

Then we have our views folder that holds our views components. The views components compose our containers. This folder combines all of our smaller pieces into one big picture, and since most of the team is experienced with some form of the model, view, controller (MVC) architecture, having a views folder gives us all a familiar entry point into the application.

The models folder contains the aforementioned NRQL models used to build queries consumed by our graph components and other containers.

The configs folder is where we store static variables that are used to configure views and containers. The libs, services, utils, andtransforms folders are where we keep smaller reusable pieces of code that format data or talk to APIs; these can even be used by our other components.

Last but not least we have our stores folder, which holds our state containers. As noted, we’re using MobX to store our state and actions.

Our end goal was to have as flat a folder structure as possible, since flat is always better than nested. We also made sure the folder names were easily readable since team members need to be able to identify what lives where and what it does.

Building more than just a UI

In addition to being a successful experiment in architecting a complex JavaScript project in React, we’ve found that this architecture has led to some critical outcomes:

  • Predictable state management: By breaking state into discrete objects, it’s easier for us to see which actions are mutating state in our components.
  • Easy for non-UI-focused team members to contribute: Building our UI architecture with object-oriented programming allows our less experienced UI devs to quickly contribute to the project.
  • Explicitness: Because we’ve decoupled state, UI components, and the underlying service models, new contributors can quickly figure out exactly what parts of the project to contribute to.
  • Ease of use: Thanks to this decoupled and organized architecture, our agent and service engineers were able to complete a significant chunk of UI work in a single sprint while the UI engineer was on a weeklong vacation.