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 theusers
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.
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
.
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.
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.
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:
- Render a button called login pressing which, Auth0 login is performed using Expo’s AuthSession.
- After the authentication is complete, the session variables are stored in
AsyncStorage
andisLoggedIn
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.