DEV Community

loading...

How to build a GraphQL app with Hasura, Postgres, and React

pmbanugo profile image Peter Mbanugo ・8 min read

In this tutorial, we're going to build a simple todo application. That'll work as you see below.

hasura-tutorial.gif

In order to follow along, an understanding of React and GraphQL is required. We will build the frontend with React and the backend to handle the data will run on Hasura Cloud and Postgres hosted on Heroku.

Setting Up The GraphQL API

We will store our data in a Postgres database and provision a GraphQL API that'll be used to add and modify data. We will use Hasura GraphQL engine to provision a GraphQL API that'll interact with the PostgreSQL database. The Postgres database will be hosted on Heroku, therefore, a Heroku account is needed. Go to signup.heroku.com/ to create an account if you don't have one.

We will create an instance of Hasura on Hasura Cloud. Hasura Cloud gives you a globally distributed, fully managed, and secure GraphQL API as a service. Go to cloud.hasura.io/signup to create an account.

Once you're signed in, you should see a welcome page.

Hasura Welcome Page

Select the Try a free database with Heroku option. You will get a new window where you have to log in to your Heroku account and grant access to Heroku Cloud. When that's done, Hasura Cloud will create an app on Heroku and install a Postgres add-on in it, then retrieve the Postgres database URL which it'll need to create the Hasura instance.

When the Heroku setup is done, you should click on the Create Project button to create an instance of Hasura.

Create The Data Model and GraphQL Schema

After the project is created, you can open the Hasura console by clicking on the Launch Console button.

console.png

This opens the Hasura admin console and it should look like what you see in the image below.

hasura-console-default.png

Our next step is to create a table to store the todo items. We will name it todos and it'll have three columns. Namely;

column name type
id Integer (PK)
task Text
completed Boolean

In order to create the table on Hasura Console, head over to the Data tab section and click on Create Table. Enter the values for the columns as mentioned in the table above, then click the Add Table button when you're done.

hasura todos table.png

When this is done, the Hasura GraphQL engine will automatically create schema object types and corresponding query/mutation fields with resolvers for the table. At this stage, our GraphQL API is done and we can focus on using it in the React app.

Bootstrap The React App

With the GraphQL API ready, we can go ahead and create the React app. We will create a new React app using create-react-app. To do this, run the command npx create-react-app hasura-react-todo-app && cd hasura-react-todo-app.

We need two packages to work with GraphQL, and they're @apollo/client and graphql. Go ahead and install it by running the command npm install @apollo/client graphql. The graphql package provides a function for parsing GraphQL queries, while @apollo/client contains everything you need to set up Apollo Client to query a GraphQL backend. The @apollo/client package includes the in-memory cache, local state management, error handling, and a React-based view layer.

Create and Connect Apollo Client to your app

Now that we have all the dependencies installed, let's create an instance of ApolloClient. You'll need to provide it the URL of the GraphQL API on Hasura Cloud. You will find this URL in the project's console, under the GraphiQL tab.

Open App.js and add the following import statement.

import { ApolloClient, ApolloProvider, InMemoryCache } from "@apollo/client";
Enter fullscreen mode Exit fullscreen mode

Then instantiate ApolloClient :

const client = new ApolloClient({
  uri: "YOUR_HASURA_GRAPHQL_URL",
  cache: new InMemoryCache(),
});
Enter fullscreen mode Exit fullscreen mode

Replace the uri property with your GraphQL server URL.

The client object will be used to query the server, therefore, we need a way to make it accessible from other components which you will create later. We will do this using ApolloProvider which is similar to React's Context.Provider. In App.js, update the component with this code:

function App() {
  return (
    <ApolloProvider client={client}>
      <div className="App">
        <header className="App-header">
          <img src={logo} className="App-logo" alt="logo" />
          <p>ToDo App</p>
        </header>
        <br />
        <TodoInput />
        <Tasks />
      </div>
    </ApolloProvider>
  );
}
Enter fullscreen mode Exit fullscreen mode

In the code you just added, you wrapped your React app in ApolloProvider. This places the client on the context, which enables you to access it from anywhere in your component tree. We have two components, TodoInput and Tasks, which you'll add shortly.

Add import statements for those components.

import Tasks from "./Tasks";
import TodoInput from "./TodoInput";
Enter fullscreen mode Exit fullscreen mode

Open App.css and update the .App class as follows

.App {
  text-align: center;
  text-align: -webkit-center;
}
Enter fullscreen mode Exit fullscreen mode

Then add a min-height: 20vh; style to .App-header.

Add Todo

Now we're going to create a component that'll be used to add new items to the list.

Add a new file TodoInput.css with the content below.

.taskInput {
  min-width: 365px;
  margin-right: 10px;
}
Enter fullscreen mode Exit fullscreen mode

Then add another file TodoInput.js and paste the code below in it.

import React, { useState } from "react";
import { useMutation } from "@apollo/client";

import { ADD_TODO, GET_TODOS } from "./graphql/queries";
import "./TodoInput.css";

const updateCache = (cache, { data }) => {
  const existingTodos = cache.readQuery({
    query: GET_TODOS,
  });

  const newTodo = data.insert_todos_one;
  cache.writeQuery({
    query: GET_TODOS,
    data: { todos: [...existingTodos.todos, newTodo] },
  });
};

export default () => {
  const [task, setTask] = useState("");
  const [addTodo] = useMutation(ADD_TODO, { update: updateCache });

  const submitTask = () => {
    addTodo({ variables: { task } });
    setTask("");
  };

  return (
    <div>
      <input
        type="text"
        placeholder="Add a new task"
        className="taskInput"
        value={task}
        onChange={(e) => setTask(e.target.value)}
        onKeyPress={(e) => {
          if (e.key === "Enter") submitTask();
        }}
      />
      <button onClick={submitTask}>Add</button>
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

Here we're using the useMutation React hook for executing mutation. We call this hook with the query to run and an update function to update the cache afterward. The updateCache function receives the current cache and the data as arguments. We call cache.readQuery to read data from the cache (rather than the server), passing it the GraphQL query string to retrieve the needed data. Then we update the cache for this query (i.e GET_TODOS) by calling cache.writeQuery with the new value for todos.

The useMutation hook returns a mutate function that you can call at any time to execute the mutation. In our case, it's called addTodo. The addTodo function is called in the submitTask function which is triggered when the Add button is clicked.

Now we have the code to perform the mutation, but we need the actual queries that'll be executed since we referenced import { ADD_TODO, GET_TODOS } from "./graphql/queries"; on line 4.

Create a new file queries.js under a new directory called graphql. Then add the following exports to it.

import { gql } from "@apollo/client";

export const GET_TODOS = gql`
  query GetTodos {
    todos {
      id
      task
      completed
    }
  }
`;

export const ADD_TODO = gql`
  mutation($task: String!) {
    insert_todos_one(object: { task: $task }) {
      id
      task
      completed
    }
  }
`;
Enter fullscreen mode Exit fullscreen mode

There you have it! The feature to add todo is done. Next up is to allow users to mark a todo as completed or delete one.

Remove Todo

Since you still have the queries.js file open, go ahead and add two more queries to remove a todo, and to toggle the completed status.

export const TOGGLE_COMPLETED = gql`
  mutation($id: Int!, $completed: Boolean!) {
    update_todos_by_pk(
      pk_columns: { id: $id }
      _set: { completed: $completed }
    ) {
      id
    }
  }
`;

export const REMOVE_TODO = gql`
  mutation($id: Int!) {
    delete_todos_by_pk(id: $id) {
      id
    }
  }
`;
Enter fullscreen mode Exit fullscreen mode

Now we need a component that'll display a todo item and allow it to be deleted, or marked as complete or incomplete. Add a new file Task.css and paste the style definition below in it.

.task {
  margin: 5px;
  border: 1px solid #282c34;
  height: 30px;
  max-width: 40vw;
  border-radius: 4px;
  display: flex;
  align-items: center;
  padding: 5px 10px;
  justify-content: space-between;
}

.completed {
  text-decoration: line-through;
}
Enter fullscreen mode Exit fullscreen mode

Add a new file Task.js with the code below.

import React from "react";
import { useMutation } from "@apollo/client";

import { GET_TODOS, REMOVE_TODO } from "./graphql/queries";
import "./Task.css";

const Task = ({ todo }) => {
  const [removeTodoMutation] = useMutation(REMOVE_TODO);

  const toggleCompleted = ({ id, completed }) => {};

  const removeTodo = (id) => {
    removeTodoMutation({
      variables: { id },
      optimisticResponse: true,
      update: (cache) => {
        const existingTodos = cache.readQuery({ query: GET_TODOS });
        const todos = existingTodos.todos.filter((t) => t.id !== id);
        cache.writeQuery({
          query: GET_TODOS,
          data: { todos },
        });
      },
    });
  };

  return (
    <div key={todo.id} className="task">
      <input
        type="checkbox"
        checked={todo.completed}
        onChange={() => toggleCompleted(todo)}
      />
      <span className={todo.completed ? "completed" : ""}>{todo.task}</span>
      <button type="submit" onClick={() => removeTodo(todo.id)}>
        remove
      </button>
    </div>
  );
};

export default Task;
Enter fullscreen mode Exit fullscreen mode

In the code above, we're using the useMutation hook for the REMOVE_TODO mutation. When the remove button is clicked, we call the removeTodoMutation function with the id of what needs to be deleted. Then use the update function to read from the cache, filter the result, and update the cache afterward.

Toggle Completed State

We will update the toggleCompleted function which is already bound to the input control on the page. We get the id and completed values and can use the useMutation function to execute the TOGGLE_COMPLETED mutation which we added in the previous section.

Import the TOGGLE_COMPLETED query.

import { GET_TODOS, TOGGLE_COMPLETED, REMOVE_TODO } from "./graphql/queries";
Enter fullscreen mode Exit fullscreen mode

Then generate a mutation function

const [removeTodoMutation] = useMutation(REMOVE_TODO);
Enter fullscreen mode Exit fullscreen mode

Now, update the toggleCompleted function:

const toggleCompleted = ({ id, completed }) => {
  toggleCompeletedMutation({
    variables: { id, completed: !completed },
    optimisticResponse: true,
    update: (cache) => {
      const existingTodos = cache.readQuery({ query: GET_TODOS });
      const updatedTodo = existingTodos.todos.map((todo) => {
        if (todo.id === id) {
          return { ...todo, completed: !completed };
        } else {
          return todo;
        }
      });
      cache.writeQuery({
        query: GET_TODOS,
        data: { todos: updatedTodo },
      });
    },
  });
};
Enter fullscreen mode Exit fullscreen mode

Display A List Of Todos

Now that we can add, display, and delete a todo, we will finally render a list of the todo items from the database. This will be quite a simple component that will query the server using the GET_TODOS query we already added, then using the useQuery hook to execute the query and pass each todo to the Task component for it to be rendered.

Let's start by adding the CSS file. Add a new file Tasks.css

.tasks {
  margin-top: 30px;
}
Enter fullscreen mode Exit fullscreen mode

Now add a new component file called Tasks.js

import React from "react";
import { useQuery } from "@apollo/client";

import { GET_TODOS } from "./graphql/queries";
import Task from "./Task";
import "./Tasks.css";

const Tasks = () => {
  const { loading, error, data } = useQuery(GET_TODOS);

  if (loading) {
    return <div className="tasks">Loading...</div>;
  }
  if (error) {
    return <div className="tasks">Error!</div>;
  }

  return (
    <div className="tasks">
      {data.todos.map((todo) => (
        <Task key={todo.id} todo={todo} />
      ))}
    </div>
  );
};

export default Tasks;
Enter fullscreen mode Exit fullscreen mode

When this component renders, the useQuery hook runs, and a result object is returned that contains loading, error, and data properties. The loading property tells if it has finished executing the query, while the error property denotes if it loaded with an error. Then the data property contains the data that we can work with. When the data is loaded, we use the Array.map function to render each todo with the Task component.

Conclusion

At this point, you have a fully functional todo application. You can start it by running the npm start command from the command line.

hasura-tutorial.gif

With what you've learnt so far, this leaves you empowered to build GraphQL powered-apps using Hasura and Apollo Client. You should now be familiar with Hasura Cloud and Hasura console, and how to connect Apollo Client to your server and use the provided hook functions to simplify querying the API and updating the UI when the data changes.

Discussion (3)

Collapse
dragos199993 profile image
Dragos Nedelcu

This is nice. I found Hasura 2 weeks ago and fell in love with it.
However I found a little bit complicated to handle authentication.
Do you consider your next article to be related to authentication? Maybe auth0 or even something custom?

Collapse
elitan profile image
Johan Eliasson

You could try Nhost (nhost.io) to get working auth (and storage) out of the box working with Hasura.

Disclaimer: I'm the founder of Nhost.

Collapse
pmbanugo profile image
Peter Mbanugo Author • Edited

My next set of articles won't be on Hasura.

Forem Open with the Forem app