Hasura 101: Building a Realtime Game with Graphql, Postgres, and React

Ali Spittel - May 13 '20 - - Dev Community

My favorite technology discovery of 2019 was Hasura. It makes getting up and running with a full Postgres + GraphQL backend a breeze -- you can pretty much click some things, and then you have a fully interactive database-explorer and editor and GraphQL endpoints you can test with GraphIQL. I want to share a tutorial on building a realtime game (with websockets!!!!) on Hasura based on a workshop I did with them earlier this year.

We'll talk about what GraphQL even is, what Hasura is, how to set up Hasura and a database, and then build a full React app on top of it. We'll build a drawing app that looks something like this:

pixel art app

Here is a deployed version of it! (Note: it's using the Heroku free tier so it may take a few seconds to boot up)

What is GraphQl

Well, according to its documentation, "GraphQL is a query language for APIs." Traditionally, with a REST API, you have all sorts of endpoints where you get access to different data or can change the data in some way. That can become pretty bulky pretty fast, and can also become a bottleneck if you're working with separate frontend and backend teams. GraphQL becomes really helpful as our apps evolve over time and need to display different data.

Sacha Grief wrote an awesome analogy in their article "So what’s this GraphQL thing I keep hearing about?".

The old REST model is like ordering pizza, then getting groceries delivered, then calling your dry cleaner to get your clothes. Three shops, three phone calls.

GraphQL on the other hand is like having a personal assistant: once you’ve given them the addresses to all three places, you can simply ask for what you want (“get me my dry cleaning, a large pizza, and two dozen eggs”) and wait for them to return.

GraphQL is also language agnostic (i.e. you can use GraphQL with any programming language), and it lives between your client and your data sources, so it's super flexible!

What is Hasura

Hasura allows you to build a GraphQL backend at lightning speed -- you can just click buttons and make some pretty awesome things.

Hasura:

  • Gives you an instant real time GraphQL API on a new or existing database.
  • It comes with dashboard that helps you set up your API and database.
  • You can react to changes in your database with web hooks, external APIs, or serverless functions on demand.
  • You can also stitch together custom GraphQL APIs and other data sources into a unified GraphQL API.

Get Up and Running with Hasura

  1. Go to this url
  2. Log in to Heroku (create an account if you haven't, don't worry it's free!)
  3. Pick a (unique) name for your application
  4. Click Deploy app
  5. ✨Magic✨! You've got a deployed Hasura instance up and running!

heroku user interface for creating a Hasura app

Set Up the Database

We're using a PostgreSQL database for our application, but Hasura gives us a super nice interface for interacting with that database.

Go to your Hasura app, which should look something like this:

Hasura GraphIQL interface

Click on the data tab, and then the create table button next to the Schema header.

Yellow create table button

We'll create a pixels table in our database to store the coordinates and colors of each pixel.

We will also create two columns in that table: id, which will be an auto-incremented integer that Postgres handles for us, and color which will store the color each pixel should take on.

Also, set id as the primary key.

Here's what your form should look like!

The form values

Then, scroll down to the bottom and click the add table button!

We now have a database with the columns we need 🎉!

Adding our Initial Data

The very first time our app loads, we want each pixel to just be a white box until people start coloring them in. To make our future code easier, we'll seed the database with 400 values that all have the color white, since the grid is a 20x20 grid.

Note: you could also store row, column, and color for each pixel, but I've found it easier to just store all 400 pixels and order them by ID on the user interface.

Within the Hasura dashboard, we can run SQL queries by clicking on the SQL link under the tables listing.

You can add your SQL to the text box that pops up and then press run!

Here is the query I ran to populate the initial database. You can copy and paste the same one to add 400 white pixels to the database!

GraphQL Queries

Now that we have data loaded into our database, we can use GraphQL to query that data. We don't need to do any more setup! You can go to the GraphIQL tab of your Hasura dashboard and play with your data.

GraphIQL is an in-browser IDE for exploring GraphQL queries. It will help you write queries to fetch and manipulate your data.

The syntax for a GraphQL query is super different from the SQL queries you may be used to -- they look more similar to JavaScript objects.

For example, to fetch all of our data, our query would look like this:

query GetPixels {
  pixels {
    id
    color
  }
}
Enter fullscreen mode Exit fullscreen mode

First, we name our query GetPixels. Then, we specify that we want to get data from our pixels table. We also say we want the data from the id and color columns. If you omitted one, you'd just get the data from that column back.

We can also change the query so that it always orders the pixels by their id's:

query GetPixels {
  pixels(order_by: { id: asc }) {
    id
    color
  }
}
Enter fullscreen mode Exit fullscreen mode

We can also write subscriptions which are queries that also subscribe to changes in the data via websockets.

Changing the word query to subscription in the above example will allow us to pull new data as it updates.

In addition, GraphQL has mutations which allow us to update data.

For example the following query will allow us to update a pixel's color given its id:

mutation changePixelColor($id: Int!, $color: String!) {
  update_pixels(where: { id: { _eq: $id } }, _set: { color: $color }) {
    returning {
      color
      id
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

This mutation is named changePixelColor, like a programming function, mutations (and queries) can take arguments. In this case, it takes id, which is an integer, and color which is a string. We need to specify the table to query, in this case pixels, which we can do by saying update_pixels. Then, we add a where clause -- we are only going to update the item in the database whose id matches the one specified. Then we specify the _set, where we say that we will set our row's color to the one specified.

Then we add a returning with the data we want sent back to our application once our query is done executing.

I would highly recommend testing out these queries in GraphIQL and using it to build custom queries -- it does a ton for you!

Here is GraphQL's documentation if you want to dive deeper on it, it's awesome!

Integration with React Code

Since this tutorial is focused more on integrating Hasura and GraphQL with an existing application, we'll start with some pre-written React code. This repository has the code we'll be building off of. Right now, it is a static version of the drawing app. One person can make pixel art, but it doesn't connect to a backend, so the drawings don't persist and people can't draw collaboratively.

If you clone down the repository, run npm install to install all of its dependencies. Give the code a quick look through to see what's going on.

Apollo Setup

We'll be using Apollo to make writing our frontend GraphQL connection easier.

In the Connection.js file add the following code:

import { HttpLink } from "apollo-link-http";
import { ApolloClient } from "apollo-client";
import { InMemoryCache } from "apollo-cache-inmemory";

export default new ApolloClient({
  cache: new InMemoryCache(),
  link: new HttpLink({
    uri: "your-endpoint.herokuapp.com",
  }),
});
Enter fullscreen mode Exit fullscreen mode

For the uri, use the GraphQL endpoint at the top of the GraphIQL tab.

GraphQL endpoint on Hasura interface

This sets up the Apollo client so our GraphQL queries will point to our endpoint we created.

We also need to add a few lines of code to index.js.

import React from "react";
import ReactDOM from "react-dom";
+import { ApolloProvider } from "@apollo/react-hooks";

import "./index.css";
import App from "./App";
import * as serviceWorker from "./serviceWorker";
import connection from "./Connection";

ReactDOM.render(
+ <ApolloProvider client={connection}>
    <App />
+ </ApolloProvider>,
  document.getElementById("root")
);

// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();
Enter fullscreen mode Exit fullscreen mode

This gives our whole application access to the GraphQL connection we created. Our queries will automatically go to the right place now.

Query Setup

We need to hit our GraphQL endpoint whenever we go to our application in order to fetch which color each pixel should be. We'll add some code to our App.js file in order to make our application fetch the data we created instead of the static data it's using now!

First, we'll import the gql template tag. This will allow us to write GraphQL queries in our JavaScript code. We'll use our query we wrote earlier to fetch all the the pixels.

const GET_PIXELS = gql`
  query GetPixels {
    pixels(order_by: { id: asc }) {
      color
      id
    }
  }
`;
Enter fullscreen mode Exit fullscreen mode

Then, we'll use the useQuery hook that Apollo provides in order to fetch our data.

const { loading, error, data } = useQuery(GET_PIXELS);

This snippet of code will run our query when our component loads.

This hook gives us three values: whether or not the query is still running (loading), the error message if one exists, and the data that comes back from the query.

Before we get our data back, we probably want some sort of loading indicator, so we'll add a condition to our component that does so:

if (loading) {
  return <h2>Loading...</h2>;
}
Enter fullscreen mode Exit fullscreen mode

We will also change our map to use the live data instead of the hard coded pixels that we're currently creating on line 5.

data.pixels.map((pixel) => (
  <Pixel {...pixel} key={pixel.id} newColor={color} />
));
Enter fullscreen mode Exit fullscreen mode

All in all, here's what changed in our App.js:

import React, { useState } from "react";
+ import { useQuery } from "@apollo/react-hooks";
+ import gql from "graphql-tag";
import Pixel from "./Pixel";
import ColorPicker from "./ColorPicker";

- const pixels = new Array(400).fill("white");

+ const GET_PIXELS = gql`
+   query GetPixels {
+     pixels(order_by: { id: asc }) {
+      color
+      id
+   }
+ }
+`;

function App() {
+ const { loading, error, data } = useQuery(GET_PIXELS);
  const [color, changeColor] = useState("white");

+ if (loading) {
+   return <h2>Loading...<h2/>;
+ }

  return (
    <div className="content">
      <div className="logo">Draw</div>
      <p>Pick a Color</p>
      <ColorPicker changeColor={changeColor} />
      <p>Click a Pixel</p>
      <div className="container">
+       {data.pixels.map(pixel => (
+         <Pixel {...pixel} key={pixel.id} newColor={color} />
+        ))}
      </div>
    </div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

Mutation Setup

Now, let's make it so that our Pixel component runs our mutation to change the pixel's color when we click on a pixel. This will make it so that our change persists across users and sessions.

We'll use our gql template tag again, and put our mutation inside it.

const UPDATE_COLOR = gql`
  mutation ChangePixelColor($id: Int!, $color: String!) {
    update_pixels(where: { id: { _eq: $id } }, _set: { color: $color }) {
      returning {
        x
        y
        color
        id
      }
    }
  }
`;
Enter fullscreen mode Exit fullscreen mode

Apollo also has a useMutation hook, so we'll import that and use it.

const [updatePixelColor] = useMutation(UPDATE_COLOR);
Enter fullscreen mode Exit fullscreen mode

We'll also update our onClick handler to run our mutation when the user clicks on the pixel.

onClick={() => {
    changeColor(color);
    updatePixelColor({ variables: { id, color: newColor } });
}}
Enter fullscreen mode Exit fullscreen mode

Here's what our Pixel.js will look like when we finish converting it:

import React from "react";
+ import gql from "graphql-tag";
+ import { useMutation } from "@apollo/react-hooks";

+ const UPDATE_COLOR = gql`
+ mutation ChangePixelColor($id: Int!, $color: String!) {
+   update_pixels(where: { id: { _eq: $id } }, _set: { color: $color }) {
+     returning {
+       color
+       id
+     }
+   }
+ }
+ `;

const Pixel = ({ id, color, newColor }) => {
+ const [updatePixelColor] = useMutation(UPDATE_COLOR);

  return (
    <span
      className="pixel"
      onClick={() => {
         changeColor(color);
+        updatePixelColor({ variables: { id, color: newColor } });
      }}
      style={{ backgroundColor: color }}
    ></span>
  );
};

export default Pixel;
Enter fullscreen mode Exit fullscreen mode

Woo! 🙌🏻 Now our application connects to our GraphQL endpoint and pulls the correct data. Here's the solution code if you want to see it in its entirety!

Making it Real Time

Right now, our application pulls from our GraphQL endpoint on page load, but it doesn't real-time update when another user clicks on a pixel. We want our users to be able to draw in real time with their friends. You can try this by opening up the deployed app in two tabs -- if you update a pixel in one tab, the other should update too.

We just need to update our App.js to use a GraphQL subscription instead of a query.

We'll use Apollo's useSubscription hook instead of useQuery and change the word query in our query to subscription. Magic 🧙🏻‍♂️!

Here's a diff showing what changed! (Note: much of the file is omitted since not much changed)

import React, { useState } from "react";
+ import { useSubscription } from "@apollo/react-hooks";
import gql from "graphql-tag";

import Pixel from "./Pixel";
import ColorPicker from "./ColorPicker";

const pixels = new Array(400).fill("white");

const GET_PIXELS = gql`
+ subscription GetPixels {
    pixels(order_by: { id: asc }) {
      color
      id
    }
  }
`;

function App() {
  const [color, changeColor] = useState("white");
+ const { loading, error, data } = useSubscription(GET_PIXELS);

...
Enter fullscreen mode Exit fullscreen mode

Here is the full code with subscriptions!

Next Steps

Conclusion

I have so much fun building applications with this stack -- it allows me to focus solely on the frontend code for simple applications. I can create an application that allows two users to interact in realtime with very little friction. Traditionally, writing a full backend with a GraphQL layer is a pretty extensive process that requires a lot of maintenance. With Hasura, we can do this with a couple clicks. This is my new go-to stack for building quick apps.

Also, here's the video version again if you want to watch!

Can't wait to see what you create!

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .