Sending Signals in Server-Driven UI

Send optimistic UI data upfront to shorten your feedback loop in Server Driven UI

At Expedia Group™, we’ve adopted an architectural paradigm called Server-Driven User Interface (SDUI). When you view your booking within the Trips pages via the mobile app or within the web browser the UI is entirely server driven. There are a number of articles on this subject, so I’d recommend you to have an understanding of this to better understand what is being solved here.

Discovery

I watched an interesting talk by JJ Qi from Capital One explaining their approach to SDUI on Mobile. He talked about a concept named "local action updates" (UI interactions that happen locally). Rather than having to re-fetch an entire view, you can have the server send down values upfront nested within an action. These have been labelled as "signals". After watching this talk I began to think about how cool it is to be able to serve future values upfront.

Why wait for the server to return a string that says "You’ve successfully saved"? when you can just give it up front.
Slide from JJ's talk
Signals mentioned in "Server Driven UI on Mobile • JJ Qi • GOTO 2020"

The arising problem

I took the concept of serving data upfront with signals and began to start playing around with it to help solve our issues in Expedia Group™. It was during a time when our pages were mostly static with no in-page interaction. All the found interactions , would lead you to a different page. However, with new product features lined up, we were quickly starting to progress towards having more dynamic interactive features such as editing your trip name.

It felt quite cumbersome to have the customer edit their trip name, post it to the server, wait for the response, and then finally re-load the view. The alternative solution is to use the existing value in which a customer supplied within the input field, and display it on the screen. This idea of instant feedback is a pattern named optimistic UI.

Optmistic UI image
Feedback loop by Simon Hearne — source

Developing signals

Inspired by Qi’s talk, the vision I had with signals was to keep it simple and follow the observer pattern. The idea is that server should be able to assign elements as subscribers and assign actions to publish an event.

Observer pattern image
"A subscription mechanism lets individual objects subscribe to event notifications." — source

When designing our signal, I knew there would be various features requiring different levels of complex interactions. One of these interactions might be updating page text, another might be removing a component off the page. It was important for me to easily identify what type of interaction it is. To communicate the different signal interactions is via defining a signal type. Defining types makes it easy to keep a catalogue of all the different interactions that can occur. It will also allow backend developers to easily understand the intent when building out the response.

Below is an example response that showcases a signal from the server.


  {
    "elements": [
      {
        "__typename": "text",
        "signal": {
          "type": "TEXT",
          "reference": null
        },
        "primary": "My cool item"
      },
      {
        "__typename": "button",
        "primary": "Click me!",
        "action": {
          "emitSignals": [
            {
              "type": "TEXT",
              "reference": null,
              "value": "My cool item just got cooler!"
            }
          ]
        }
      }
    ]
  }

Figure 1 — example SDUI response with Signals

In figure 1 we can see that there are two different elements (show by "__typename") being returned; "text" that has a signal assigned within it, and "button" that has an action to emits signals. In this example, "text" is subscribing to the signal type TEXT, when a user clicks on the button it will emit a signal event that will pass the value "My cool item just got cooler!" to all of its subscribers that’s listening to it. We can now allow the server to compose some interesting results where it can influence objects to have different values.

Signal request flow
Without having to request an external source to update the value, we can update immediately with Signals.

However, in figure 1's example, the client application will have to understand that the string value being emitted from the button should update the "primary" field. Having "value" map to "primary" requires client-side logic and won’t communicate clearly when writing out the response from the server-side. It would require a lot of precognitive knowledge. It also doesn’t allow for a lot of flexibility as we can only send a single string value to the subscribers, what if you want to update multiple fields within an object?

In real-world scenarios, objects can be shaped in any given way. However, the constraint I made was that a signal event should not emit an object that replaces it entirely. To better explain, imagine you have many different GraphQL types of UI elements which contains signals. For you to emit a value to replace that object, you’d have to union a bunch of different element types to the emit value for it to work. It just wouldn’t scale. A better way to do this is to use a key-value pair.


  {
    "elements": [
      {
        "__typename": "text",
        "signal": {
          "type": "TEXT",
          "reference": null
        },
        "primary": "My cool item"
      },
      {
        "__typename": "button",
        "primary": "Click me!",
        "action": {
          "emitSignals": [
            {
              "type": "TEXT",
              "reference": null,
              "values": [
                {
                  "key": "PRIMARY",
                  "value": {
                    "__typename": "SignalStringValue",
                    "value": "My cool item just got cooler!"
                  }
                }
              ]
            }
          ]
        }
      }
    ]
  }

Figure 2 — example SDUI response with key-value pair

Emit signals with key-value pair

Since we use the signal type to help identify what it is, we can also leverage this to help define its shape. With that understanding, we can change "value" within emit signals to have an array of values that builds an object via a key and value field. You can then leverage these fields to map a key to a particular field within the subscribed object. This means when creating a key-value pair for emitting, you can be confident that what you’ll be mapping to should follow all objects that match the defined signal typeTEXT. For this to work well, subscribed elements should be atoms. This is so that they stay close to only having primitive values defined for their visual display.

Signal key map image
Using keys to map to fields 1:1 when emitting a signal.

However, the value within a key-value pair cannot be as simple as a string/integer/float. It will need to have types that define some rules for the client. For something simple, such as replacing text, we can tell the client that this value type is a string. For getting form input values we need some client-defined logic therefore a new value type should be introduced.


  type SignalKeyValuePair {
    key: String!
    value: SignalValue
  }

  union SignalValue = SignalStringValue | SignalFieldIdValue | SignalFieldIdExistingValues

Figure 3 — GraphQL schema types for SignalKeyValuePair

When editing a trip name, a customer is presented with a form. The form will have inputs with values we’d want to use for instant feedback. To make it work we introduced SignalFieldIdValue (figure 3). In this new type the server will populate a field input ID within the initial response. This ID is later used when a customer submits the form. Upon firing that submit event, we use that ID to retrieve the customer’s inserted value within the input and emit that value to the listening signals.

Edit trip flow image
Flow from left to right on editing a trip name leveraging signals
Tip name and description image
Figure 4 — Trip name and description

We also introduced another type for dealing with arrays. Looking at figure 4, below the trip name (Miami) we have the dates for the trip, and then a description. The dates and the description are kept within an array. When editing the trip, the customer can only edit the name as well as the description. The challenge was how to update the description and not affect the dates whilst keeping it server-driven. The solution was to create a type that has the field id, but also an array with prefix values. We can then join the values together into a single array and emit a result that matches the original order; dates then description.


  type SignalFieldIdExistingValues {
    ids: [String!]!
    prefixes: [String!]
  }

GraphQL schema type for updating values in an array from field inputs.

Going with this approach allows us to have some flexibility with the key-value pair but still gives us some constraints to prevent bloating the value with many different shapes.

Emit to one or many subscribers

In figures 1 and 2 we took a look at a single subscriber based on the signal type TEXT. So, what if we had multiple subscribers listening to TEXT? In this case, all subscribers will receive the same values when a signal emits and will all update simultaneously. This works great for sharing values across the UI. However, what if you just want to update a single subscriber that’s still related toTEXT? This is what reference is used for. You can define particular references you wish to point to when emitting, this way if you have an array of the same type, you can point to a specific reference you want to update.

Signals sharing same type image
Signals sharing the same type
Signals using reference image
Signals using reference

Future vision

Our primary use case for signals is to present an optimistic UI, and it is used in production today by editing your trip name. We now have customers getting immediate feedback from their changes rather than having to wait for a response back from the server. This lets the customer move on to do other tasks within their trip whilst the actual updating can be kept in the background.

Signals allow us to be able to provide dynamic responses upfront. We can make changes without having to rely on waiting on the server’s response. However, the idea of signals is not limited to single responses. It can allow multiple responses to communicate with one another within the same view because you’re registering a subscriber that will listen to an event that can occur anywhere in the view. It can allow for multiple teams to work on their own modules for a view, but allows for a way to communicate between them.

If you’re interested to learn more taking a deep dive about SDUI, you can read this GitHub documentation which is my opinionated way of working with SDUI as well as learning more about signals.