DEV Community

Hasura for Hasura

Posted on • Originally published at blog.hasura.io on

3 1

Tutorial: Fullstack React Native with GraphQL

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

Edit notice: This blogpost was updated on 1st March, 2019 for updating the deprecated parts.

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:

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 the open source Hasura GraphQL Engine that gives instant Realtime GraphQL over Postgres.

Deployment

  • Deploy Hasura GraphQL Engine by simply clicking on the button below.

Hasura on Heroku
Click this button to deploy the GraphQL engine to Heroku

  • Note the URL of the deployed app. It should be of the form: myfancyapppname.herokuapp.com. 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   |
+--------+-----------------------------+
Enter fullscreen mode Exit fullscreen mode

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 for learning 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) |
+--------------+---------------------------------------------------+
Enter fullscreen mode Exit fullscreen mode

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. 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://myfancyapp.herokuapp.com/v1alpha1/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
Enter fullscreen mode Exit fullscreen mode

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:

import React from 'react';
import { StyleSheet, Text, View, AsyncStorage } from 'react-native';
import Auth from './src/auth/Auth';
export default class App extends React.Component {
state = {
isLoggedIn: false,
userId: null,
username: null,
jwt: null,
loading: false
}
login = (userId, username, token) => {
this.setState({
isLoggedIn: true,
userId,
username,
jwt: token,
loading: false
});
}
logout = () => {
this.setState({
isLoggedIn: false,
loading: false
})
}
async componentDidMount() {
AsyncStorage.getItem('@todo-graphql:auth0').then((session) => {
if (session) {
const obj = JSON.parse(session);
if (obj.exp > Math.floor(new Date().getTime() / 1000)) {
this.login(obj.id, obj.name, obj.token)
} else {
this.logout();
}
} else {
this.logout()
}
})
}
render() {
const { isLoggedIn, userId, username, loading } = this.state;
if (loading) {
return <View><Text>Loading...</Text></View>
}
if (isLoggedIn) {
return (<View style={styles.container}><Text> Hello {username} </Text></View>)
} else {
return (<Auth login={this.login}/>)
}
}
}
view raw fsrn_appjs_1.js hosted with ❤ by GitHub

Part 3: Setup Auth

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

npm install --save jwt-decode
Enter fullscreen mode Exit fullscreen mode

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.

import { AuthSession } from 'expo';
import React from 'react';
import {
Alert,
StyleSheet,
Text,
View,
AsyncStorage,
TouchableOpacity,
Image
} from 'react-native';
import jwtDecoder from 'jwt-decode';
const auth0ClientId = 'a38qnFo6lFAQJrzkun--wEzq3jVNGcWW';
const auth0Domain = 'https://yourdomain.auth0.com';
const toQueryString = (params) => {
return '?' + Object.entries(params)
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
.join('&');
};
export default class App extends React.Component {
state = {
isLoggedIn: false
}
loginWithAuth0 = async () => {
// get redirect URL to redirect after log in
const redirectUrl = AuthSession.getRedirectUrl();
// perform login
const result = await AuthSession.startAsync({
authUrl: `${auth0Domain}/authorize` + toQueryString({
client_id: auth0ClientId,
response_type: 'id_token',
scope: 'openid profile',
audience: 'https://graphql-tutorials.auth0.com/api/v2/',
redirect_uri: redirectUrl,
nonce: "randomstring"
}),
});
console.log(result);
// if success, handle the result
if (result.type === 'success') {
this.handleParams(result.params);
}
}
handleParams = (responseObj) => {
// handle error
if (responseObj.error) {
Alert.alert('Error', responseObj.error_description
|| 'something went wrong while logging in');
return;
}
// store session in storage and redirect back to the app
const encodedToken = responseObj.id_token;
const decodedToken = jwtDecoder(encodedToken);
AsyncStorage.setItem(
'@todo-graphql:auth0',
JSON.stringify({
token: encodedToken,
name: decodedToken.nickname,
id: decodedToken.sub,
exp: decodedToken.exp
})
).then(() => {
this.props.login(decodedToken.sub, decodedToken.nickname, encodedToken)
})
}
render() {
return (
<View style={styles.container}>
<View>
<TouchableOpacity
style={styles.loginButton}
onPress={this.loginWithAuth0}
>
<Text style={styles.buttonText}> Login </Text>
</TouchableOpacity>
</View>
</View>
);
}
}
view raw fsrn_auth.js hosted with ❤ by GitHub

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
Enter fullscreen mode Exit fullscreen mode

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://myfancyappname.herokuapp.com/v1alpha1/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;
Enter fullscreen mode Exit fullscreen mode

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
  }
});
Enter fullscreen mode Exit fullscreen mode

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:

import React from 'react';
import { StyleSheet, Text, View, AsyncStorage } from 'react-native';
import createApolloClient from './apollo';
import gql from 'graphql-tag';
import { ApolloProvider } from 'react-apollo';
import TodoList from './TodoList';
export default class Main extends React.Component {
state = {
client: null
}
async componentDidMount() {
const client = createApolloClient(this.props.token);
await 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
}
});
this.setState({
client
});
this.props.logout()
}
render () {
if (!this.state.client) {
return <View><Text>Loading...</Text></View>;
}
return (
<ApolloProvider client={this.state.client}>
<TodoList
userId={this.props.userId}
username={this.props.username}
logout={this.props.logout}
/>
</ApolloProvider>
);
}
view raw fsrn_main.js hosted with ❤ by GitHub

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}/>)
    }
}
Enter fullscreen mode Exit fullscreen mode

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
    }
  }
`}
>
Enter fullscreen mode Exit fullscreen mode
  • 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>
Enter fullscreen mode Exit fullscreen mode
  • 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:

import React from 'react';
import {
FlatList,
View,
StyleSheet
} from 'react-native';
import { Query } from 'react-apollo'
import gql from 'graphql-tag';
const FETCH_TODOS = gql`
query {
todos {
id
text
is_completed
}
}
`;
export default class TodoList extends React.Component {
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}) => <Text>item.text</Text>}
keyExtractor={(item) => item.id.toString()}
/>
</ScrollView>
)
}
}
</Query>
)
}
}

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:

import React from 'react';
import {
StyleSheet,
Text,
TextInput,
TouchableOpacity,
View,
} from 'react-native';
import gql from 'graphql-tag';
import {Mutation} from 'react-apollo';
import { FETCH_TODOS } from './TodoList';
const INSERT_TODO = gql`
mutation ($text: String!, $userId: String!){
insert_todos (
objects: [{
text: $text,
user_id: $userId,
}]
){
returning {
id
text
is_completed
}
}
}
`;
export default class Textbox extends React.Component {
state = {
text: '',
}
render() {
const { text } = this.state;
const { userId } = this.props;
return (
<Mutation
mutation={INSERT_TODO}
variables={{
text,
userId,
}}
update={(cache, {data: {insert_todos}}) => {
const data = cache.readQuery({
query: FETCH_TODOS,
});
const newTodo = insert_todos.returning[0];
const newData = {
todos: [ newTodo, ...data.todos]
}
cache.writeQuery({
query: FETCH_TODOS,
data: newData
});
}}
>
{
(insertTodo, { loading, error}) => {
const submit = () => {
if (error) {
return <Text> Error </Text>;
}
if (loading || text === '') {
return;
}
this.setState({
text: ''
});
insertTodo();
}
return (
<View style={styles.inputContainer}>
<View style={styles.textboxContainer}>
<TextInput
style={styles.textbox}
editable = {true}
onChangeText = {this._handleTextChange}
value = {text}
/>
</View>
<View style={styles.buttonContainer}>
<TouchableOpacity style={styles.button} onPress={submit} disabled={text === ''}>
<Text style={styles.buttonText}> Add </Text>
</TouchableOpacity>
</View>
</View>
);
}
}
</Mutation>
);
}
_handleTextChange = (text) => {
this.setState({
text
})
}
}
view raw fsrn_textbox.js hosted with ❤ by GitHub

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>
    );
}
Enter fullscreen mode Exit fullscreen mode

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:

<script src="https://gist.github.com/wawhal/b2bc438c225c6b96064a387655a7b56a.js"></script>
Enter fullscreen mode Exit fullscreen mode

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>
    )
  }
Enter fullscreen mode Exit fullscreen mode

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.

SurveyJS custom survey software

Build Your Own Forms without Manual Coding

SurveyJS UI libraries let you build a JSON-based form management system that integrates with any backend, giving you full control over your data with no user limits. Includes support for custom question types, skip logic, an integrated CSS editor, PDF export, real-time analytics, and more.

Learn more

Top comments (0)