Tutorial: Fullstack React Native with GraphQL

A tutorial to build a React Native to-do app with Apollo’s new Query and Mutation components

Overview

In this tutorial, we will be building a React Native to-do app that helps us add tasks, view them, mark/unmark them as complete and delete them.

To build the app, we will be using:

  • React Native
  • Apollo Client (for client-side GraphQL)
  • Hasura GraphQL Engine (for GraphQL backend)
  • Auth0 (for auth)

Note: We will be using the new Query and Mutation components that Apollo introduced in their 2.1.3 release of react-apollo.

Part 1: Deploying a GraphQL backend

We need a GraphQL backend where we can store the state of our app. We will be using Hasura Cloud to generate an instant GraphQL backend.

Deployment

  • Deploy Hasura GraphQL Engine by simply clicking on the button below.
  • Note the URL of the deployed app. It should be of the form: <auto-generated-string>.hasura.app. This is your GraphQL Engine URL.

Creating the tables

For storing the user information , we will create a users table.

users
+--------------+------------------------------+
|  column      |       type                   |
+--------------+------------------------------+
| id           | serial NOT NULL primary key  |
| name         | text NOT NULL primary key    |
+--------------+------------------------------+

Here is the significance of the columns:

  • id: This is a unique integer that will identify each entry in the users table. It is also the primary key of the table.
  • name: This is the name of the user

The data for this table will be coming from Auth0.

Note: Setting up Auth0 and integrating with Hasura has already been done and it is beyond the scope of this tutorial. Click here to learn how to do it.

For storing our todos, we will need a todos table with the following fields.

todos
+--------------+---------------------------------------------------+
|  column      |       type                                        |
+--------------+---------------------------------------------------+
| id           | serial NOT NULL primary key                       |
| task         | text NOT NULL                                     |
| is_completed | boolean NOT NULL                                  |
| user_id      | integer NOT NULL FOREIGN KEY REFERENCES users(id) |
+--------------+---------------------------------------------------+

Here is the significance of the columns:

  • id: This is a unique integer that will identify each todo. It is also the primary key of the table.
  • text: This is the to-do task.
  • is_completed: This is a boolean flag that marks the task as completed and pending.
  • user_id: This is a foreign key referencing id of the users table. It relates the todo to its author.

Let us create the above tables in our backend:

  • Go to your GraphQL Engine URL in your browser (https://<auto-generated-string>.hasura.app). It opens up an admin UI where you can manage your backend.
  • Go to the Data section on top and click on “Create Table” and add the aforementioned column names and types.
Creating todos table
Creating todos table

Table relationships

As you see above, there is meant to be a foreign-key based relationship between todos and users . Lets add the foreign key constraint and the relationship. Go to the Data tab on top and click on todos table. Now, in the modify section, edit the user_id column and make it a foreign key. After this go back to the Data tab and click on Track all relations .

Adding foreign keys
Adding relationships

Once you track the relationship, you can make complicated nested GraphQL queries to https://<auto-generated-string>.hasura.app/v1/graphql . To try out, go to the GraphiQL tab in the console and try making a query.

GraphiQL - An API Explorer

Table permissions

In our todos table, we want users to CRUD only their own todos. Hasura provides an access control layer for setting up rules to restrict data to specific roles. In this app, we will have only user role. Let us set permissions for it.

Go to /data/schema/public/tables/user/permissions in your Hasura console and enter the role user and allow CRUD in the user table only when x-hasura-user-id is equal to id . This means that Hasura will ensure that a user can CRUD only when the X-Hasura-User-Id from the JWT in the header is equal to the id of the user that they are CRUDing over.

Setting insert permission for users table

The above screenshot shows the permission condition for insert query, add similar permissions for select , update and delete queries.

Similarly, add permissions for todos table with a condition: { 'user_id': 'X-Hasura-User-Id' } . This means that a user can CRUD only their own todos.

With this, we have set up our backend. Let us work on React Native now.

Part 2: Setup React Native Project

We will be using Expo for this tutorial. Get started with a boilerplate project by running:

npm install -g expo-cli
expo init Todo
cd Todo
npm start

This will create an empty React Native project where App.js is the entry point. This App.js must maintain a state called isLoggedIn which if false, it should renders the Auth screen, else render the the app (currently just Hello world). It should also pass login and logout functions as props to the AuthScreen and the app respectively. The App.js should currently look something like:

Part 3: Setup Auth

Since we are using JWT, install the package jwt-decode from npm.

npm install --save jwt-decode

Create a directory called src at the top level and create another subdirectory in it called auth. Inside auth , create a file called Auth.js and perform authentication with auth0 using Expo’s AuthSession. The Auth0.js should look something like this.

The above component does the following:

  1. Render a button called login pressing which, Auth0 login is performed using Expo’s AuthSession.
  2. After the authentication is complete, the session variables are stored in AsyncStorage and isLoggedIn of the parent component is set to true so that the app is navigated to the app.

Once auth is complete, next, we have to instantiate Apollo client for client side GraphQL.

Configuring Apollo Client

Firstly, let us install the dependencies related to Apollo client. Run the following command from the todo-app directory.

$ npm install apollo-boost react-apollo graphql-tag graphql --save

Create a file called apollo.js and export a funtion that accepts a token and returns an instance of Apollo Client. You have to configure the Apollo client with the GraphQL endpoint and the token. (Replace with your own GraphQL endpoint)

import { ApolloClient } from 'apollo-client';
import { HttpLink } from 'apollo-link-http';
import { InMemoryCache } from 'apollo-cache-inmemory';
const GRAPHQL_ENDPOINT = `https://<auto-generated-string>.hasura.app/v1/graphql`;
const createApolloClient = (token) => {
  const link = new HttpLink({
    uri: GRAPHQL_ENDPOINT,
    headers: {
      'Authorization': `Bearer ${token}`
    }
  });
  return new ApolloClient({
    link,
    cache: new InMemoryCache()
  })
}
export default createApolloClient;

Now create a directory in the src folder called app and create a file called Main.js . This will be the entrypoint of your todo app where you instantiate the Apollo client using the above function and provide it to the children components using ApolloProvider . The child component currently is just TodoList. We will write this component in the next section.

Before that, we have to insert the user that logged in into the users table with an insert_mutation using the client. We can do it in the componentDidMount of this component itself before setting client in state.

client.mutate({
  mutation: gql`
    mutation ($username: String, $userid: String){
      insert_users (
        objects: [{ name: $username, id: $userid}]
      ) {
        affected_rows
      }
    }
  `,
  variables: {
     username: this.props.username,
     userid: this.props.userid
  }
});

Note: gql from graphql-tag is like a query parser that parses a graphql-string into an AST document that Apollo client understands.

Your Main.js should look something like this:

Also modify the render of App.js to pass the appropriate flag.

render() {
    const { isLoggedIn, userId, username, loading, jwt } = this.state;
    if (loading) {
      return <View><Text>Loading...</Text></View>
    }
    if (isLoggedIn) {
      return (
        <Main
          userId={userId}
          username={username}
          token={jwt}
          logout={this.logout}
        />
      )
    } else {
      return (<Auth login={this.login}/>)
    }
}

Creating our first Query component

Lets write our TodoList component. We will use Apollo’s Query components to fetch all the todos from the server. Lets see how to use the Query component

The flow goes like:

  • import {Query} from 'react-apollo';
  • import gql from 'graphql-tag'; graphql-tag is just like a parser that parses a GraphQL query into
  • Pass the GraphQL query string as prop to the Query component.
<Query query={gql`
  query {
    todos {
      id
      text
      is_completed
    }
  }
`}
>
  • Wrap your custom component inside the Query component.
<Query query={gql`GRAPHQL_QUERY`}>
  {(data, error, loading) => {
   return (<MyComp data={data} error={error}, loading={loading} />)
  }}
</Query>
  • MyComp in the above component receives the state and response of the GraphQL query.

We will write our TodoList component similarly. Create a file called TodoList.js in the src directory. Write a TodoList using the Query component, similar to what is shown above. It will look something like:

The above component simply fetches all the todos and renders their text in a FlatList.

Writing our first Mutation component

Mutation components work just like the Query components except, they also provide a mutate function that can be called whenever you want. In case of mutations, we also need to update the UI after the mutation succeeds.

Insert todos

Create a file called Textbox.js and add the following content to it:

In the above component, we use the <Mutation> component that provides a render prop with a function to insert todo. The Mutation component also takes an update prop which takes a function to update the Apollo cache after the mutation success.

Update the render method of the Main.js component to render the above Textbox as well.

render () {
    if (!this.state.client) {
      return <View><Text>Loading...</Text></View>;
    }
    return (
      <ApolloProvider client={this.state.client}>
        <Textbox
          userId={this.props.userId}
          username={this.props.username}
          logout={this.props.logout}
        />
        <TodoList
          userId={this.props.userId}
          username={this.props.username}
          logout={this.props.logout}
        />
      </ApolloProvider>
    );
}

Update and delete todos

As of now, we are just rendering the todo text in the FlatList. We also want the ability to mark the todo as complete and to delete the todos. To do this, we will render each todo item as a separate component instead of just the text. In this component, we can have the marking complete functionality and the delete functionality.

Create a file called TodoItem.js . It would look something like this:

The above component again uses the Mutation components and we follow the same flow that we did while inserting todos. If you observe well, you will notice that we have not updated the cache in case of update mutation. This is because, Apollo cache automatically updates the items if it is able to match the id of a mutation response with the id of an item in the cache.

Finally, update the render method of TodoList.js to render the above TodoItem in the Flatlist.

render() {
    return (
      <Query
        query={FETCH_TODOS}
      >
        {
          ({data, error, loading}) => {
            if (error || loading) {
              return <View> <Text> Loading ... </Text> </View>
            }
            return (
              <ScrollView style={styles.container} contentContainerStyle={styles.container}>
                <FlatList
                  data={data.todos}
                  renderItem={({item}) => <TodoItem todo={item}}
                  keyExtractor={(item) => item.id.toString()}
                />
              </ScrollView>
            )
          }
        }
      </Query>
    )
  }

Wrapping up

We covered the following in this blogpost

  • Deployed a GraphQL server in the form of Hasura GraphQL Engine
  • Set up tables and permissions
  • Set up a React Native project and performed auth using Auth0.
  • Set up Apollo client with a GraphQL endpoint and JWT
  • Use Apollo’s Query components to fetch todos
  • Use Apollo’s Mutation components

We did not:

  • Use Hasura’s GraphQL Subscriptions
  • Implement a logout button
  • Go into styles of React Native. All code snippets are more like pseudo code code snippets.

Hasura gives you instant GraphQL APIs over any Postgres database without having to write any backend code.

Blog
07 Sep, 2022
Email
Subscribe to stay up-to-date on all things Hasura. One newsletter, once a month.
Loading...
v3-pattern
Accelerate development and data access with radically reduced complexity.