Workday Prism Analytics: The Search for a Strongly-Typed, Immutable State

By Michael Habib, Software Development Engineer, Workday

As does any application in today’s ever-changing front-end landscape, the UI framework of Workday Prism Analytics has undergone a series of transformations as it has evolved. Today, our Workday Prism Analytics front-end is set to undergo another exciting shift towards a more modular architecture.

Enter React. We chose React from the plethora of JavaScript platforms to simplify development through its standardized framework and to maximize performance through its virtual DOM, which updates the real DOM only for necessary change-sets. Most importantly, React’s component-based architecture enables microservices throughout Workday to contribute to and consume from a centralized library of reusable components.

In regards to a view model, we wanted a single source of truth for state transparency and update clarity, so Redux seemed like a fitting state-management pattern. Here, components dispatch actions — objects composed of a type and payload — that are processed by pure functions known as reducers. The reducers in turn update the store, or state, of the application. We then use Reselect selectors to read necessary properties from our store in a modular fashion, hiding all state access from the components themselves.

Finally, for the developmental advantages of a strongly-typed system, and to keep consistent with the rest of the Workday ecosystem, we chose the JavaScript superset TypeScript as our base language, posing the following question: how can we implement an immutable Redux store while adhering to TypeScript’s strongly-typed syntax?

An example of Workday Prism Analytics’ UI

Plain Objects

The most lightweight option, using native JavaScript objects to maintain Redux state, requires no additional API knowledge or external dependencies. Here, to enforce an immutable store, each update must copy state to a new object, rather than merely mutating the original state.

With this approach, there is unfortunately no way, other than developer due diligence, to guarantee absolute immutability. Accidental state mutations can slip through reviews undetected and cause complications. Complex linters to detect state assignment can help ameliorate some woes here, but none are perfect. Finally, with complex state, nested updates likely will result in multiple levels of copying and can quickly escalate into slow and unmaintainable operations.

Immutable.js

Immutable, a library developed and maintained by Facebook, has received a lot of praise for its performant and powerful immutability framework. The library works by mapping native JavaScript objects and arrays to custom, immutable data structures called Maps and Lists, respectively. Implemented through hash array-mapped tries, these structures have guaranteed immutability and performant updates.

At first glance, Immutable seems like an extremely promising option, but can it conform to a strongly-typed language? Let’s dive a little deeper.

First, Immutable adds a layer of abstraction with its custom structures, meaning developers need to learn a new, slightly verbose API for setting and getting properties. Additionally, since we wish to create reusable components that other teams can consume, we need to avoid introducing external dependencies in the components themselves. Therefore, Immutable must be restricted to our reducers and selectors.

To avoid dependencies, we must convert our JavaScript state to and from Maps and Lists. Immutable provides two convenient methods, fromJS and toJS, which do just that, albeit with a price. Neither method preserves objects’ type signatures, and both have quite the performance impact (see the performance chart below), two detrimental pitfalls.

Immutable.js with Records

We can ameliorate several of these issues by using another Map-like object introduced by Immutable: Record. With a bit of overhead, we can implement custom Records with explicit types represented in our state, allowing us to avoid using fromJS in state initialization.

Sample Immutable Record
Sample Immutable State in Reducer

Now, our state not only has all the benefits Immutable offers, but it is also strongly typed, so methods such as set, get, and merge can realize TypeScript’s static type-checking.

Unfortunately, we still run into issues here with nested structures; neither setIn nor getIn, used to update and get nested properties, preserves types. Using either of these extremely useful methods again sacrifices our beneficial type-checking, and implementing a combination of get, set, and mergeDeep, if possible, can quickly become unmaintainable. Furthermore, Records do not solve our problem with toJS’s performance when mapping state properties to components in our selectors.

Seamless-Immutable

Next, a library similar to Immutable on the surface, Seamless-Immutable introduces a subtly different type of immutable structure. Seamless’ structures, unlike Immutable, are backwards-compatible with JavaScript objects and arrays, effectively eliminating any need to convert objects; the added dependency here should have no effect on our functional components. Hence, Seamless solves our Immutable performance issue of complex object conversion.

This library has some detrimental flaws, however, that prevent us from seriously considering it for Workday Prism Analytics’ use case. First, when trying to update or read nested state properties, Seamless runs into the same problem of type-loss that we saw with Immutable Records. Second, as of this writing, Microsoft Internet Explorer 9 (IE) and later doesn’t support this library in its entirety. Workday supports IE 9 and later, and maintaining a separate immutability framework solely for IE seems extremely impractical.

Immer

Exploring another option, Immer is a lightweight library written by Michel Weststrate (creator of the Redux alternative MobX), which works by using proxies and copy-on-write. Given an initial state, it produces a proxied draft that we can freely modify and ultimately returns a new object, leaving our initial state unchanged. As a result, Immer guarantees immutability. Additionally, due to its use of structural sharing and proxies, Immer processes updates as performantly as Immutable.

Immer offers other benefits. For one, it has pure API simplicity. Immer consists of one, straightforward function (demonstrated below) that is easily consumable. Most importantly, since we are dealing with native JavaScript objects and arrays, property types are preserved throughout, regardless of nesting level. Finally, our selectors need not worry about object conversion or its accompanying performance impact.

Sample Reducer Using Immer

This library comes with its drawbacks as well. IE11 does not support proxies, meaning Immer needs to rely on its significantly slower es5 fallback, a potentially significant issue for complex state updates. Fortunately, Immer’s simplicity makes it completely opt-in on a reducer by reducer basis, so native objects — or another alternative — could be selectively used if need be.

Second, Immer is still in its infancy. Michel Weststrate published Immer roughly three months ago (as of this writing), and it does not have the backing support of a large company like Immutable does via Facebook. Issues, should they arise, have the potential of going unaddressed.

The performance benchmark, from Immer, of various reducer implementations. It demonstrates both the inefficiency of toJS and Immer in es5, and the efficiency of handwritten reducers, pure Immutable, and Immer, the latter two being only negligibly slower than the former. Note that we can safely ignore freeze metrics here.

Closing Thoughts

We explored several options here on our search for a strongly-typed, immutable application store, but these are not by any means the only ones available. In fact, Redux Ecosystems alone lists over 65 packages to help with immutable data structures. Of these, immutability-helper and immutable-assign also stood out as other viable options, the latter seeming similar to, albeit lesser known than, Immer.

Given this vast multitude of frameworks and helper packages, it would be impossible to examine each one in detail. We considered certain libraries because of their prevalence in the front-end community, simplicity, reliability, and potential for TypeScript compatibility. Other frameworks may better suit different needs, especially if there is no desired support for TypeScript or IE.

In the end, we chose Immer for Workday Prism Analytics’ immutability use case. We set out at the start to find a framework, or lack thereof, that guaranteed an immutable store, while integrating seamlessly with TypeScript’s strongly-typed syntax. Despite its downsides, Immer not only fulfills both of these requirements, but is also lightweight, simple, and generally performant. Thus far, developers enjoy using Immer; it has been extremely non-intrusive and easy to uptake with little-to-no learning curve.

As we’ve seen, strongly-typed immutability has quite a few possible solutions, each with its own benefits and downsides. With the prevalence of so many intersecting technologies, hopefully this article helps to simplify your choices in building a modern, scalable front-end application.