Inbox: Powering our Front-End through a Data-Driven Model
How data feeds into our product’s front-end architecture via GraphQL queries
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
To start off, I’ll define a simplified front-end representation of Inbox in order to abstract away some of the specific contextual details and make it easier to familiarize with.
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
A powerful advantage of GraphQL is the ability to declaratively fetch data in the shape that you ask for and know in confidence that you can always expect it to return predictable results. This is perfect for abstracting away complex backend details from the front-end since all we need to know is the schema definition.
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 anopenLinkAction
- mention: will also have tone
Special
and action will be anopenProfileAction
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”
Once we have our queries set up and fetching the data that we need, it’s time to consider how we want to handle this data through the front-end.
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 Elementconst 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
Hootsuite Inbox is a data-driven product that is powered by GraphQL. Our front-end architecture relies on these queries to process and display hundreds of thousands of messages every single day. We talked about how being able to declare the shape of your data responses through GraphQL is a valuable asset to Inbox. But as with anything, there are always drawbacks. If you have a large schema, maintaining it can become difficult as you start to develop complex relationships such as nesting and recursion within your GraphQL types. With REST, each individual endpoint is reasonably isolated, making it easier to work in changes and identify issues, whereas with GraphQL you’ll need to carefully ensure that your schema is well-designed or risk encountering some massive headaches.
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.