Building HandsUp: An OS Real-Time Q&A App Using GraphQL and React
Using GraphQL Subscriptions, Apollo Client 1.0 and Auth0
In this article, we will look at all the steps that were involved to build an Open Source real-time Q&A App using GraphQL and React.
- Solution Architecture: HandsUp App
- GraphQL Server: create it using Graphcool CLI
- Apollo Client: bootstrap setup and React integration
- Queries: displaying questions
- Mutations: voting and tracking votes using polling
- Authenticating Users: Auth0 setup, React integration, Graphcool User integration and displaying logged User
- Subscriptions: adding new questions and subscribing to them
This App will allow attendees of an event to ask questions (if logged in) and vote for the most interesting ones.
You can see the final result below:
All users will be able to vote questions, increasing their importance. In order to add new questions users must login. As new questions and votes are being recorded, users are updated. All this information can then be used by event organisers to run Q&A sessions or panels during an event.
You can access the final solution in GitHub.
Find the latest GraphQL content following my feed at @gerardsans.
Solution Architecture: HandsUp App
In order to implement these features, we will use GraphQL. This is also to show how we can use its real-time features by using Subscriptions.
For an introduction to GraphQL and Apollo Client, you can read this blogpost.
GraphQL and the amazing Apollo Client
On the server side, we are going to use Graphcool as our GraphQL Server.
On the client side, we are going to use Apollo Client as our bridge between React and GraphQL Server. We will use Auth0 so users can login to our app using their Social Accounts.
Finally, we will add GraphQL Subscriptions to achieve real-time updates for new questions.
See an overview of our Architecture below:
Building blocks for our real-time voting Application
Creating the GraphQL Server
Let’s start creating our GraphQL Server. This may sound quite daunting but by using Graphcool CLI, is super easy. Go ahead and sign up before doing the next steps.
We will use Graphcool CLI to create our Data Model (schema). Run these commands to install the CLI and create a new Project.
npm install --global graphcool
graphcool init
Follow the instructions on screen, and remember to sign up first. This will create the GraphQL Server to host our data. The inital schema is auto-generated. Update the project.graphcool
file, created during the init command, with the latest schema available here and run the commands below. First one will update the schema. You can use the second one to open Graphcool Console.
graphcool push
graphcool console
Note: latest schema may include more fields and types as more features are added to the project
This is the final Data Model we will be using for this article.
Graph View in Graphcool Console
Once you are more familiar with GraphQL, you may want to create your own GraphQL Server. That’s totally fine. Read this blogpost by Jonas Helfer to learn more.
Apollo Client
Apollo Client is a framework-agnostic GraphQL client that helps you fetch data and keep your client state in sync with the server.
Bootstrap Setup
In order to setup Apollo Client we need to add some dependencies to our project.
npm install --save apollo-client react-apollo graphql-tag
This will install the required dependencies to run our GraphQL queries. We will create a separated file client.js
to hold our Apollo Client setup.
// src/client.js
import ApolloClient, { createNetworkInterface } from 'apollo-client'
const networkInterface = createNetworkInterface({
uri: 'https://api.graph.cool/simple/v1/YOUR_KEY_HERE',
dataIdFromObject: record => record.id,
})
export const client = new ApolloClient({
networkInterface,
})
In order to get your key you can run
graphcool endpoints
using Graphcool CLI.
From react-apollo
we will use ApolloProvider, a high order component during bootstrap. We need to pass the client we just set up using the attribute with the same name.
// src/app.js
import { ApolloProvider } from 'react-apollo'
import { client } from './client'
render(
<ApolloProvider client={client}>
<HashRouter>
<Route path='/' component={HandsUpApp} />
</HashRouter>
</ApolloProvider>,
document.getElementById('root')
)
We only need a route pointing to our main component.
React integration
Whenever we need to use GraphQL , we will extend our components using this pattern below
import {graphql} from 'react-apollo'
import {QUERY_OPERATION} from './graphql/query.operation.gql'
class Component extends React.Component {}
const withOperation = graphql(QUERY_OPERATION, {options})
export default withOperation(Component)
By doing so, we can easily extend our components adding specific GraphQL properties (props) that we can then use to interact with our GraphQL Server.
There’s some setup required to your webpack config to import
.graphql
or.gql
extensions. Check graphql-tag/loader. The main benefit is avoiding processing GraphQL AST’s (Abstract Syntax Tree) on the client versus using gqlquery
.
Displaying questions
We will start by looking at the data model necessary to handle questions. This will be a new type named Question. You can see a simplified definition below. Each question will have an id, a body and when it was created.
type Question {
id: ID!
body: String!
createdAt: DateTime!
}
In order to handle the list of questions, we created QuestionList and Question components. See a simplified pseudo-html version below:
// src/components/QuestionList.js
<QuestionList questions={questionList}>
<ul>
<Question key={question.id} question={question}>
<li>{question.body}</li>
</Question>
...
</ul>
</QuestionList>
In the snippet below we define a query to fetch all available questions. The question fragment will allow us to reference the same question fields in other queries and/or mutations.
query questions {
allQuestions {
...question
}
}
fragment question on Question {
id
body
createdAt
}
Voting
Getting the questions from the audience during an event is key, but for large events there might be some time constraints, so it may be useful to get feedback on which questions are the most popular. We can achieve that by allowing attendees to vote.
We need to extend our model with another type Vote. Each vote will be linked to a question.
type Vote {
id: ID!
createdAt: DateTime!
question: Question
}
Let’s see what the voting mutation looks like:
// src/graphql/Vote.mutation.gql
mutation createVote($question: ID!) {
createVote(questionId: $question) {
id
}
}
This mutation will create a new entry using the question id. Let’s see how we can build this mutation into our App. We are going to extend the Question component, that renders a question, with a new mutation
// src/components/Question.js
const withVote = graphql(CREATE_VOTE_MUTATION,
{
props: ({ ownProps, mutate }) => ({
vote(id) {
return mutate({
variables: { question: id },
})
},
}),
},
)
export default withVote(Question)
This will extend the props object to include a vote(id)
function making the call to mutate and passing the question id by passing the question id within the variables property.
Within the Question component we will trigger this mutation when the user clicks on the voting button for that question. Notice how we used optimistic UI by applying the change immediately by changing the state.
// src/components/Question.js
class Question extends React.Component {
onSubmit() {
this.setState({ votes: this.state.votes+1 })
this.props.vote(this.props.question.id)
}
// <button onClick={e => this.onSubmit()}></button>
}
Tracking votes information
So far we covered how we added new votes for a question, but we didn’t cover how we will keep all this data up-to-date for all questions.
In order to do that we are going to use a nice feature from Graphcool called Aggregations. This feature allow us to keep the current count of votes for each question without doing any extra coding! For each type, Graphcool creates an extra type that we can use to access this data, in our case this is _votesMeta
.
query questions {
allQuestions {
...question
}
}
fragment question on Question {
id
body
createdAt
_votesMeta { count }
}
If we take a look at the question fragment, we can see some new fields _votesMeta
and count
. This will allow us to gather information regarding how many votes a specific question has.
Question Votes Polling
We covered how we keep track of new votes and how we retrieve the current vote count. But we didn’t covered how we get new updates into our UI.
To display the voting information, we are going to setup a polling to get this information on a fixed schedule. This is a compromise to avoid over fetching. Synchronising votes in real-time would generate unnecessary amounts of traffic for large events.
// src/components/QuestionList.js
const withQuestions = graphql(QUESTIONS_QUERY,
{
options: { pollInterval: POLLING_TIME },
props: ({ data }) => {
return {
questions: data.allQuestions,
}
},
},
)
export default withQuestions(QuestionList)
By using polling, we make sure all users are up-to-date every few seconds with the latest aggregated information. As this information may not be totally accurate, we replaced the total count of votes for a volume-like display. The more bars, the more votes a question has, following an exponential function.
Authenticating users using Auth0
Auth0 Setup
Sign up at Auth0 website and create a new SPA client. We will use this SPA client to access Auth0 services from our React App.
You can follow this blog for more details on how to setup an Auth0 account and how to get the CLIENT_ID
and DOMAIN
.
To handle user log-in and session management we created the Authorisation service.
// src/services/Authorisation.js
export default class Authorisation {
constructor() {
this.lock = new Auth0Lock(CLIENT_ID, DOMAIN, {
auth: {
responseType: 'id_token',
params: { scope: 'openid email' },
redirect: false,
},
})
this.lock.on('authenticated', this.doAuthentication.bind(this))
}
authenticate() {
this.lock.show()
}
}
From the code above, we are using Auth0Lock
to provide a nice UI to go through all the steps for the user to log in. This is the setup to use an overlay pop-up with no redirects. After setting up the Auth0Lock
instance, we registered a callback for the authenticated
event. To display this Lock pop-up we created a separate authenticate
method.
Let’s look into what happens once the user logs-in and the authenticated
event is triggered.
// src/services/Authorisation.js
doAuthentication(authResult) {
if (!this.profile) {
this.auth0IdToken = authResult.idToken
this.lock.getProfile(authResult.idToken, (error, profile) => {
if (error) {
this.auth0IdToken = null
this.profile = null
} else {
this.profile = profile
}
})
}
}
The first thing we do is store the auth0IdToken
in the localStorage wrapped within a getter and setter for convenience.
// src/services/Authorisation.js
get auth0IdToken() {
return localStorage.getItem('auth0IdToken')
}
set auth0IdToken(value) {
if (value) {
localStorage.setItem('auth0IdToken', value)
} else {
localStorage.removeItem('auth0IdToken')
}
}
After that we will try to get access to the user profile using this token. This is an asynchronous operation so we pass a callback and use the same approach to store it in the localStorage. In case of any errors we just clear up the user information.
Integration with React
In order to integrate it with our App, we are going to use a High Order Component that will pass the Authorisation
class instance reference down the component tree.
// src/app.js
const auth = new Authorisation()
class HandsUpAppWrapper extends React.Component {
render() {
return (
<HandsUpApp auth={auth} {...this.props} />
)
}
}
We can use the auth0IdToken
for authorisation purposes when communicating with our GraphQL Server. In order to integrate the two, we can use a middleware to add the required headers.
// src/client.js
networkInterface.use([{
applyMiddleware(req, next) {
if (localStorage.getItem('auth0IdToken')) {
if (!req.options.headers) {
req.options.headers = {}
}
req.options.headers.authorization =
`Bearer ${localStorage.getItem('auth0IdToken')}`
}
next()
},
}])
Once we have this setup, we can configure specific queries to only allow Authorised users to carry them out.
Graphcool User Integration
In order to use the User information in our Model, we need to register new users by using the createUser
mutation provided by Graphcool. The most important field is idToken provided by Auth0. The remaining fields will allow us to display the user profile picture and username.
// src/graphql/CreateUser.mutation.gql
mutation createUser(
$idToken: String!,
$name: String!,
$username: String!,
$pictureUrl: String!
){
createUser(
authProvider: {
auth0: {
idToken: $idToken
}
},
name: $name,
username: $username,
pictureUrl: $pictureUrl
) {
id
}
}
Notice the id
field at the bottom. See below a simplified version of the User type.
type User {
id: ID! # graphcool internal id
auth0UserId: String # auth0 idToken
name: String
username: String
pictureUrl: String
createdAt: DateTime!
questions: [Question!]!
}
We are going to use that to change the question mutation so we include the User logged in.
// src/graphql/CreateQuestion.mutation.gql
#import "./Question.fragment.gql"
mutation addQuestion($body: String!, $user: ID!) {
createQuestion(body: $body, userId: $user) {
...question
}
}
Questions. Displaying User Data
Because we use the same question fragment both in the query and the mutation, we can simply extend it to fetch the new fields.
// src/graphql/Question.fragment.gql
fragment question on Question {
user { id username pictureUrl }
}
Displaying Logged User
Once we have the User information from Auth0, we can use the Authorisation service to provide this information to our components.
In order to display the logged User, we are going to create a Profile component and place it in our TopNavigation component. This will be a pure component only depending on profile
and isLogged
props.
// src/components/TopNavigation.js
<Profile
profile={this.props.auth.profile}
isLogged={this.props.isLogged}
/>
The component results in a straightforward implementation.
// src/components/Profile.js
class Profile extends React.Component {
render() {
if (!this.props.isLogged) {
return null
}
return (
<div className='profile'>
<img src={this.props.profile.picture} />
</div>
)
}
}
Adding new questions
So far we have covered all the features for anonymous users. Anyone with access to our application can follow the list of questions and vote. Let’s add the option for attendees to add new questions.
See below the mutation query to create a new question.
// src/graphql/CreateQuestion.mutation.gql
#import "./Question.fragment.gql"
mutation addQuestion($body: String!, $user: ID!) {
createQuestion(body: $body, userId: $user) {
...question
}
}
We can use the logged User to create a question and link it to a registered User in the system.
Let’s see how we integrated this mutation query to the AddQuestion component
// src/components/AddQuestion.js
class AddQuestion extends React.Component {
onSubmit(event) {
event.preventDefault()
this.props
.addQuestion(this.input.value, this.props.auth.userId)
}
render() {
return (
<form onSubmit={e => this.onSubmit(e)}>
<input ref={node => (this.input = node)} />
<button type='submit'>Send</button>
</form>
)
}
}
const withAddQuestion = graphql(CREATE_QUESTION_MUTATION, {...})
export default withAddQuestion(AddQuestion)
On the code above, we can see how we linked the onSubmit
form event to the addQuestion
passing the input value and the current logged user.
Notice how we got access to the underlying input element by using a ref attribute callback.
// src/components/AddQuestion.js
const withAddQuestion = graphql(CREATE_QUESTION_MUTATION,
{
props: ({ mutate }) => ({
addQuestion(body, id) {
return mutate({
variables: { body: body, user: id },
updateQueries: {
questions: (state, { mutationResult }) => {
let newQuestion = mutationResult.data.createQuestion
return update(state, {
allQuestions: {
$push: [newQuestion],
},
})
},
},
})
},
}),
},
)
In this instance we are adding the addQuestion
function to our component as a new property (prop). We will call mutate
passing the question body and user id as variables
. When the mutation comes back with results updateQueries
will execute. We can then access the result by using mutationResult
and returning the new state.
To avoid adding side-effects, we are using immutability-helper
library. You can learn about its syntax here.
Setting up Subscriptions
In order to access the real-time features, we need to add subscriptions support to our Apollo Client. Make sure to run this command:
npm install — save subscriptions-transport-ws
Apollo Client Subscriptions Setup (client.js)
We also require a separate endpoint from Graphcool to access Subscriptions on the server.
In order to get your subscription key, you can run
graphcool endpoints
using Graphcool CLI.
// src/client.js
import { SubscriptionClient, addGraphQLSubscriptions } from 'subscriptions-transport-ws'
const wsClient = new SubscriptionClient('wss://subscriptions.graph.cool/v1/YOUR_KEY_HERE', {
reconnect: true,
})
const networkInterfaceWithSubscriptions = addGraphQLSubscriptions(
networkInterface,
wsClient
)
export const client = new ApolloClient({
networkInterface: networkInterfaceWithSubscriptions,
})
On the code above, we set up the SubscriptionClient
using your endpoint. We activated the reconnect
flag, so the client will recover gracefully from transport failures. Finally, we extend our current network interface to include the subscriptions client.
Subscribing to new questions
One key feature for our Application is that new questions are shared between all attendees in real-time, so everyone can learn about them and vote.
Let’s take a look at our subscription query:
// src/graphql/Questions.subscription.gql
#import "./Question.fragment.gql"
subscription {
Question(filter: { mutation_in: [CREATED] }) {
node {
...question
}
}
}
The syntax for subscriptions requires a type and a list of operations we are interested in: CREATED, UPDATED or DELETED. In the code above we are subscribing to new Questions. So for each new question we will receive a message (node) with all fields defined in our question fragment.
Let’s see how we integrated this subscription with the QuestionsList component that displays the list of questions.
// src/components/QuestionList.js
class QuestionList extends React.Component {
componentWillMount() {
this.props.subscribeToNewQuestions()
}
render() {...}
}
const withQuestions = graphql(QUESTIONS_QUERY, {...})
const withSubscription = graphql(QUESTIONS_QUERY, {...})
export default withSubscription(withQuestions(QuestionList))
We are extending the QuestionList Component using the same pattern as before with the new subscription. Notice how we used componentWillMount
to trigger the subscription. See the code for subscribeToNewQuestions
below.
// src/components/QuestionList.js
const withSubscription = graphql(QUESTIONS_QUERY,
{
props: ({ data: { subscribeToMore } }) => ({
subscribeToNewQuestions() {
return subscribeToMore({
document: QUESTIONS_SUBSCRIPTION,
updateQuery: (state, { subscriptionData }) => {
const newQuestion = subscriptionData.data.Question.node
if (!isDuplicate(newQuestion.id, state.allQuestions)) {
return update(state, {
allQuestions: {
$push: [newQuestion],
},
})
}
},
})
},
}),
},
)
We are using subscribeToMore
to define our updateQuery
. This will execute every time we receive a message from our subscription query QUESTIONS_SUBSCRIPTION
.
We can get hold of the new Question data using the subscriptionData
property. The output for updateQuery
should be the resulting new state including the new question.
To avoid adding side-effects, we are using immutability-helper
library. You can learn about its syntax here.
Because we used optimistic UI we need to check for duplicates to avoid double entries for the user adding the question.
You can access the final solution in GitHub.
That’s all folks! Have any questions? Thanks for reading! Ping me at @gerardsans