Inbox: Powering our Front-End through a Data-Driven Model

Jessica Wu
Apr 24 · 6 min read

How data feeds into our product’s front-end architecture via GraphQL queries

Image for post
Image for post

Hootsuite Inbox is a product that allows customers to engage with their audiences through several various social media platforms: all in one consolidated place. Every day, Inbox handles around 20k inbound and outbound messages across a set of different social networks such as Facebook, Twitter, and LinkedIn. To manage such large flows of data, we maintain what we call a data-driven approach that leverages GraphQL queries to communicate with and power our React front-end. In this article, I will walk through how this front-end orchestration works its wonders for Inbox.


Laying out the Pieces

import React from 'react'
import ...
export class SimpleInbox extends React.Component {
render() {
return (
<Container>
<ConversationsList /> // the list of all conversations
<SelectView> // the currently selected one
<Header />
<Content />
</SelectView>
</Container>
)
}
}

Let’s also define how the Content component is structured:

import ...export const Content = ({ content }) => {
const { elements, text } = content
return (
<Container>
<TextWithElements {... { elements, text }} />
</Container>
)
}

In Inbox, and as with many other products, there are stylistic distinctions that apply to a visual component based on the set of data received. The way you would render plain text that makes up a message very likely differs from the way you would render its associated author’s name. This is especially the case if there are other intentions or actions that are linked to that component, such as being able to click on a name and navigate to that user’s profile. That being said, it seems to be the most intuitive to have the front-end respond instinctively to handle these differences in data. Perhaps it would help to think of the front-end structure as an “empty shell” that exists for the retrieved data to populate and define behaviour for. This is exactly the data-driven model that serves as the fundamental backbone of our Inbox front-end. Essentially, we have our GraphQL back-end handle all the data, and then propagate it up to the front-end to fill in the spaces.

Shaping our Data: Leveraging GraphQL

Let’s refer back to our simplified representation. Say we want to render the original, top-level message of a selected conversation. This message will contain regular text but also a link and a username mention that needs to be stylized accordingly. How should we approach this in a data-driven way?

Defining a schema to represent how we want to shape this data might be a good start:

# provides information about the message content
type Content {
text: String
elements: [TextElement]
}
# specifies the type of text element and its stylistic properties
type TextElement {
start: Int
length: Int
tone: Tone
action: Action
}

Let’s consider this example message:

"Hey @jessica, check out this cool article! medium.com/123"

The Content type contains the entire message as text, but also stylized elements that correspond to sections of that text that are special such as @jessica and medium.com/123 .

These elements are of type TextElement and have indices that represent their start position in the text and the length that they span. They also have a tone to represent how we want it stylized and an action that we associate them with. For the username handle, we expect to be able to click it and open profile details, and for a link we expect it to open up that link.

Here, we’ll focus on three cases of a TextElement:

  • simple plain text: tone will be Normal and action will be no action
  • link: will have tone Special and action will be an openLinkAction
  • mention: will also have tone Special and action will be an openProfileAction

With that set in place, we can query for these exact pieces of data and then wire up our components to handle the cases respectively. We can enhance this querying logic by using GraphQL fragments, which are constructed sets of fields that you can reuse in your queries. You can envision them to be kind of like helper functions. Fragments are useful for grouping up fields that are commonly fetched for together, or for simply reducing complexity in components that would require fetching from multiple sources. An otherwise convoluted query with repetitive fields can be streamlined by allowing the use of many fragments combined within a single fetch call.

Here is an example of how we would query for our message:

query getSelectedMessage($messageId: String!) {
conversation(messageId: $messageId) {
...contentFragment
}
}
fragment contentFragment on Content {
text
elements {
...textElementFragment
}
}
fragment textElementFragment on TextElement {
start
length
tone
action {
...//some other fragment for action
}
}

Filling in Our Front-End “Shell”

Diving back into theContent component that we introduced at the beginning, we have a TextWithElements component nested within it.

const Content = ({ content }) => {
const { elements, text } = content
return (
<Container>
<TextWithElements {... { elements, text }} />
</Container>
)
}

In this TextWithElements component, we can define a function, replaceElements, that will map over each text element and replace it with its respective styling applied.


const TextWithElements = ({ elements, text }) => {
if (text == null) return null
if (elements == null || (!elements.size && !elements.length))
return <span aria-hidden>{text}</span>
return replaceElements(elements, text).map((component, i) =>
React.cloneElement(component, { key: i }),
)
}
const replaceElements = ...
// function that then takes the GraphQL data that we provide and maps it to the corresponding Element
const Element = ({ action, text, tone }) => {
if (
tone === Tones.SPECIAL &&
action &&
action.__typename === OpenLinkAction.TYPE
) {
return LinkElement({ action, text, tone })
} else if (
tone === Tones.SPECIAL &&
(action && action.__typename === OpenProfileAction.TYPE)
) {
return <Mention {...{ action, text, tone }} />
} else if (action == null && tone != null) {
return DefaultTextElement({ text, tone })
}
return UnknownElement()
}

The specific implementation details of the function are removed in order to decouple from its underlying complexity. However, you can see that based on the action and tone props that are received, we return either a link element, a mention element, or a default text element. Similarly, this can be applied with any spread of data: we can fill in our front-end components by asking for exactly what we need and nothing more or less. The beauty of this all comes down to that aspect of being able to define the structure of our data. This is in comparison to standard REST where often times you need to make several calls to different endpoints just to fetch a complete set of data for a component. Not to mention, with the fixed data structure of these endpoints, you’ll most likely also end up fetching for a lot of excess, unnecessary fields.

Concluding Thoughts


Thanks for reading!

About the Author

Jessica is a software developer co-op on the Engage team at Hootsuite. She is a 4th year computer science student at the University of British Columbia. When she’s not studying or working, she finds excitement in organizing and participating in hackathons from all over.

Hootsuite Engineering

Hootsuite's Engineering Blog

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch

Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore

Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store