Managing Application State in Duo Insight
Duo Insight is our new free, easy-to-use risk assessment tool to help companies identify their users and devices that may be vulnerable to phishing attacks. One of the key user interface components of Duo Insight is the campaign wizard, where users can quickly and easily configure a phishing campaign using one of several pre-made services.
We had a few goals in mind when designing and developing the wizard. First, we wanted to create an enjoyable UI, which needed to be fast, responsive and reliable. Second, Duo has a fluid and iterative development process, so we needed tools that would help us refactor quickly. Third, and perhaps most importantly, we needed to be able to address the growing complexity of application state in the wizard.
State - in any application - can quickly become a nightmare that ruins developers’ lives. Without discipline, server and user data ends up scattered around the application with no logical connection or structure, potentially increasing the surface for bugs and complicating refactoring.
For example, consider the workflow below, where a user starts to craft their phishing email. We have to think about such state as applications in use (and not in use) by a company, custom configurations for each application type, the current step in the campaign wizard, and so on. As the number of applications grow, so does the challenge of keeping state organized.
The Flux Approach to State
We identified React as a solid tool to meet our development needs. React is an open-source library created at Facebook that uses modular components to build out an application’s view layer. Facebook suggests building React-based applications in a Flux pattern, which is a great way to deal with problems of state.
Flux favors a unidirectional flow of data through your application. Stores should retain all (or most) of the application state. When Store data changes, they indicate to your React Views that the View should update itself with the new state. When the View wants to change application state, an Action is fired that changes data in the Store in some way. And the cycle repeats.
image via https://facebook.github.io/flux/docs/overview.html
Facebook is mostly agnostic on the tools you actually use to implement the Flux pattern. To handle managing state we decided to use models from Backbone.js, a library with a relatively established history and community, and already used extensively throughout Duo’s products. Backbone.js provides the functionality one would expect from a Flux store, including emitting granular change events and serializing data into a view-friendly format. The implementation also ends up being relatively simple and allows for a great deal of flexibility.
Let’s look at one feature of the wizard. Suppose the user exits from the campaign wizard before completion. When they return to the wizard, they can resume at the point where they left off with all of their work intact. Because application state is managed in one section of our application, we could easily implement and automate this behavior.
Saving and Restoring State
To make this happen, we save the application state in the client’s local storage after changes to a Store. The base implementation of this type of model in Backbone looks like the following:
Any Model that extends off of this
LocalStorageModel will save its contents to localStorage every time its data changes. Here is a simplified example:
Going through this example, we do a few things:
- We extend a Backbone model off of the
- We initialize the model; a Store should act as a singleton in our application.
- We describe how the model should behave given particular actions emitted from the React View. For example, after the user sets their name, it is stored in the model. This emits a change event, which causes the model to save itself in localStorage, as well as informing the View to update itself (we will see this next).
- If the user wants to resume their campaign, we restore the model from local storage. Otherwise, we clear the data.
The View listens for changes in our Store, and updates itself when the Store changes. A simplified example of this is like the following:
Every time our Store changes, we merge our Store data into the React component’s internal state by calling the appropriately named method
setState. To get a fresh copy of the Store data, we call the Backbone
toJSON method. After
setState is called, the React component will re-render itself, and our view layer is now synchronized with our application state.
This architecture has really paid off for us when working on Duo Insight. The simplicity of the React API means that the learning curve feels less steep than other popular libraries; Backbone has nicely complemented that with its corresponding ease of use. As we scale by adding new phishing services and configurations we don’t have to worry too much about application state spinning out of control, since it is nicely organized and managed by concrete Stores and Actions. Proving out React in Duo Insight is also leading us to examine its potential benefits in developing our Trusted Access platform.