Full-Stack React Native with Hasura and Firebase

A step-by-step guide to setting up a React Native app with Firebase authentication and cloud functions synced with a Hasura database with GraphQL APIs.

Full-Stack React Native with Hasura and Firebase

A step-by-step guide to setting up a React Native app with Firebase authentication and cloud functions synced with a Hasura database with GraphQL APIs.

Over the past month or so, I have built two apps that use this same stack and everything seems to be working pretty well so far. So I thought, why not ask people if they'd like to read about the implementation and see what the response is like. And the response was surprisingly positive, to a point where more people interacted with that tweet than they did with my pinned tweet. 😔

So here I am, writing this post and hoping it helps you build out your next great idea. Let's take a look at the tools we'll be using throughout this tutorial:

  • Hasura has been my go-to tool for every project I've done since the second half of last year. It's super easy to get started and all you have to do is create your database models and you get free GraphQL APIs. The awesomeness doesn't stop there, you can do custom roles and permissions, merge remote schemas, and trigger webhooks on database triggers.
  • Firebase has grown on me over the past couple of months. I wasn't a huge fan because I had to use their database product in the past and it was pretty painful. To be honest, it still is. But, we don't care about that, we want to use firebase for their authentication and the cloud functions platform it provides.
  • Expo - If you're reading a React Native post, you probably already know what Expo is and how helpful it is. Their managed workflow is a dream if you're just starting out and the bare workflow is pretty handy to have if you know your way around a React Native app.

Also, I think this is going to be a huge post and it is very easy to get overwhelmed but if you take it slow and try to understand why we are doing the things that we are doing, it's pretty straightforward. And, feel free to tweet at me if you have any questions or even suggestions and I'd be more than happy to talk.

Firebase Authentication

Let's start by initializing our application. If you don't already have Expo, you'd need to install it and if you already do, upgrading it is a good idea as they're always making improvements. Regarding the workflows, I am personally opting for the blank managed workflow (you can always eject to bare) but you can choose the one you want as it doesn't make any difference for what we're doing in this tutorial.

# Install the CLI
npm install --global expo-cli

# Initialize a new project
expo init full-stack-react-native

The first thing we're going to tackle is implementing authentication using Firebase. To do that, we're going to use:

yarn add firebase react-native-dotenv tailwind-rn

To finish the setup for react-native-dotenv, make sure you update your babel config.

module.exports = function (api) {
  api.cache(true);
  return {
    presets: ['babel-preset-expo', 'module:react-native-dotenv'],
  };
};
babel.config.js

At this point, I am assuming you have a firebase web app created. If you don't, just set up a new firebase project and add a web app to it. You also need to go under authentication and enable the email/password auth.

Alright, let's set up firebase with the proper credentials. Start by creating a .env file in the root directory. Don't forget to add this file to your .gitignore.

API_KEY="YOUR_API_KEY"
AUTH_DOMAIN="YOUR_AUTH_DOMAIN"
DATABASE_URL="YOUR_DATABASE_URL"
PROJECT_ID="YOUR_PROJECT_ID"
STORAGE_BUCKET="YOUR_STORAGE_BUCKET"
MESSAGE_SENDER_ID="YOUR_MESSAGE_SENDER_ID"
APP_ID="YOUR_APP_ID"
.env

Next, create lib/firebase.js to initialize and use the SDK.

import firebase from 'firebase';
import {
  API_KEY,
  AUTH_DOMAIN,
  DATABASE_URL,
  PROJECT_ID,
  STORAGE_BUCKET,
  MESSAGE_SENDER_ID,
  APP_ID,
} from 'react-native-dotenv';

const config = {
  apiKey: API_KEY,
  authDomain: AUTH_DOMAIN,
  databaseURL: DATABASE_URL,
  projectId: PROJECT_ID,
  storageBucket: STORAGE_BUCKET,
  messagingSenderId: MESSAGE_SENDER_ID,
  appId: APP_ID,
};

const Firebase = firebase.initializeApp(config);

export default Firebase;
firebase.js

Before we move on, let's think about our flow for a bit. We want our users to be able to sign up and log in to our app and once they do that, we want to show them some content and also allow them to log out. To handle all these screens and the navigation between them, we are going to use React Navigation.

To install react-navigation, simply follow the docs based on the Expo workflow you are using. If you, like me, went with the managed workflow, simply follow along.

yarn add @react-navigation/native

expo install react-native-gesture-handler react-native-reanimated react-native-screens react-native-safe-area-context @react-native-community/masked-view

yarn add @react-navigation/stack

Next, let's set up some very basic navigation for our app. I think everything here is pretty straightforward but I've still added some comments to explain what is going on. If the fact that this makes your app crash bothers you, just set up some dummy components for Home, Signup, and Login to get the app running.

import React, { useState } from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { createStackNavigator } from '@react-navigation/stack';

// We will be creating these files soon
import Home from './screens/Home';
import Signup from './screens/Signup';
import Login from './screens/Login';

const RootStack = createStackNavigator();
const AuthenticationStack = createStackNavigator();

const App = () => {
  // We will come back to this and check and set the user from Firebase
  const [user, setUser] = useState(null);
  return (
    <NavigationContainer>
      {/* If we have a user, take them to the app */}
      {user && (
        <RootStack.Navigator
          screenOptions={{ headerShown: false }}
        >
          <RootStack.Screen name="Home" component={Home} />
        </RootStack.Navigator>
      )}
      {/* If no one is logged in, take them to our authentication screens */}
      {!user && (
        <AuthenticationStack.Navigator
          screenOptions={{ headerShown: false }}
        >
          <AuthenticationStack.Screen name="Signup" component={Signup} />
          <AuthenticationStack.Screen name="Login" component={Login} />
        </AuthenticationStack.Navigator>
      )}
    </NavigationContainer>
  );
};

export default App;
App.js

Now that we have some basic navigation set up, we can start working on the Signup screen so our users can create accounts.

import React, { useState } from 'react';
import {
  SafeAreaView, Text, View, TextInput, TouchableOpacity, Alert,
} from 'react-native';
import tailwind from 'tailwind-rn';

import Firebase from '../lib/firebase';

const Signup = ({ navigation }) => {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');

  const handleSignup = async () => {
    try {
      {/* You should ideally do some sort of validation before this
      Think password length and so on */}
      await Firebase.auth().createUserWithEmailAndPassword(email, password);
    } catch (error) {
      console.error(error);
      Alert.alert('Error', error.message);
    }
  };
  return (
    <SafeAreaView style={tailwind('flex-1 justify-center')}>
      <View style={tailwind('py-10 px-5')}>
        <Text style={tailwind('text-4xl font-bold')}>
          Sign up
        </Text>

        <View style={tailwind('mt-10')}>
          <TextInput
            placeholder="Email"
            onChangeText={(val) => setEmail(val)}
            autoCapitalize="none"
            style={tailwind('text-lg border-b-2 border-blue-500')}
          />
          <TextInput
            placeholder="Password"
            onChangeText={(val) => setPassword(val)}
            autoCapitalize={false}
            secureTextEntry
            style={tailwind('text-lg border-b-2 border-blue-500 mt-5')}
          />

          <TouchableOpacity
            style={tailwind('bg-blue-500 rounded-lg py-3 mt-10')}
            onPress={handleSignup}
          >
            <Text style={tailwind('text-white text-center font-bold text-lg')}>
              Sign up
            </Text>
          </TouchableOpacity>
        </View>

        <View style={tailwind('mt-2 flex-row justify-center')}>
          <Text>Already have an account?</Text>
          <TouchableOpacity
            style={tailwind('ml-1')}
            onPress={() => navigation.navigate('Login')}
          >
            <Text style={tailwind('text-blue-500')}>
              Login
            </Text>
          </TouchableOpacity>
        </View>
      </View>
    </SafeAreaView>
  );
};

export default Signup;
Signup.js

Once you do this and try signing up, you will notice that the user gets created properly on Firebase but nothing happens on our app. This is because we aren't checking to see if we have a user available before we show our authentication screen.

Firebase Signup

So let's fix that and update our code so that we're showing the right screen based on whether a user is logged in or not. Go ahead and plug the following lines into your App.js. Make sure you don't replace your entire file as this is only part of the code we should be adding to the file while keeping what we already had.

...
import Firebase from './lib/firebase';
...

const App = () => {
  const [user, setUser] = useState(null);
  useEffect(() => {
    Firebase.auth().onAuthStateChanged((user) => {
      if (user) {
      	{/* Set a user if someone is logged in */}
        setUser(user);
      } else {
        {/* Set user to null, we do this to handle log outs */}
        setUser(null);
      }
    });
  }, []);
  
  ...
};

export default App;
App.js

Once we plug this code in and try signing up again, we should be able to successfully sign up and be redirected to our home screen! Now that we're done with the signup screen, let's quickly go ahead and do our login screen as well.

import React, { useState } from 'react';
import {
  SafeAreaView, Text, View, TextInput, TouchableOpacity, Alert,
} from 'react-native';
import tailwind from 'tailwind-rn';

import Firebase from '../lib/firebase';

const Login = ({ navigation }) => {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');

  const handleLogin = async () => {
    try {
      await Firebase.auth().signInWithEmailAndPassword(email, password);
    } catch (error) {
      console.error(error);
      Alert.alert('Error', error.message);
    }
  };
  return (
    <SafeAreaView style={tailwind('flex-1 justify-center')}>
      <View style={tailwind('py-10 px-5')}>
        <Text style={tailwind('text-4xl font-bold')}>
          Login
        </Text>

        <View style={tailwind('mt-10')}>
          <TextInput
            placeholder="Email"
            onChangeText={(val) => setEmail(val)}
            autoCapitalize="none"
            style={tailwind('text-lg border-b-2 border-blue-500')}
          />
          <TextInput
            placeholder="Password"
            onChangeText={(val) => setPassword(val)}
            secureTextEntry
            style={tailwind('text-lg border-b-2 border-blue-500 mt-5')}
          />

          <TouchableOpacity
            style={tailwind('bg-blue-500 rounded-lg py-3 mt-10')}
            onPress={handleLogin}
          >
            <Text style={tailwind('text-white text-center font-bold text-lg rounded-lg')}>
              Login
            </Text>
          </TouchableOpacity>
        </View>

        <View style={tailwind('mt-2 flex-row justify-center')}>
          <Text>Not a member yet?</Text>
          <TouchableOpacity
            style={tailwind('ml-1')}
            onPress={() => navigation.navigate('Signup')}
          >
            <Text style={tailwind('text-blue-500')}>
              Sign up
            </Text>
          </TouchableOpacity>
        </View>
      </View>
    </SafeAreaView>
  );
};

export default Login;

Once we set this up, we should be able to log into the app with our registered email and password!

Firebase Login

With that, we're done with the first part of this tutorial. That wasn't so difficult, was it? You can even go ahead and add the reset password functionality to this if you'd like as Firebase takes care of sending the email and resetting the password and you simply have to ask the user for their email and call the Firebase API.

Hasura

Alright, now that we're done with authenticating our users using Firebase, let's move on to Hasura. Before we get started with the implementation, let's talk about the things we're going to cover in this section of the tutorial.

  • Setting up - We are going to set up a Hasura instance with a user model with proper permissions so users can only update their own data.
  • Configuring JWT claims - When our users sign up, we want to set up the proper JWT claims on our users. We need to do this because we want our users to have the right permissions and only be able to access their own data.
  • Syncing Firebase users with Hasura - When a user signs up to our app using Firebase, we want to add that user to our Hasura database. We also want to be able to remove users that delete their accounts on our app.
  • Setting up GraphQL - We will also go over how to set up GraphQL within our React Native app so we can interact with our Hasura database using queries and mutations.

Setting up

To get started, simply visit the Hasura website and select the service you want to deploy your app on and follow the documentation accordingly. I am going to go with Heroku because it is free and serves the purpose of this tutorial.

Deploying a Hasura instance on Heroku

Once that's done, we want to secure our Hasura database. If you're using Heroku, go to the settings menu in your project dashboard to add a new environment variable. For any other service, the idea is the same. We simply have to add a new environment variable called HASURA_GRAPHQL_ADMIN_SECRET. I'd recommend adding a secure password here as this is the password you will be using to access your database.

While we're in the business of adding environment variables, let's also configure and add our JWT secret. Create a new environment variable called HASURA_GRAPHQL_JWT_SECRET and set the value of this variable to the snippet below.

{
  "type":"RS256",
"jwk_url":"https://www.googleapis.com/service_accounts/v1/jwk/[email protected]",
  "audience": "<your-firebase-project-id>",
  "issuer": "https://securetoken.google.com/<your-firebase-project-id> "
}
JWT Secret Configuration

Make sure you update the audience and the issuer fields with your actual project ID.

Okay, so we're done with securing our database and also configuring our JWT secret. Let's move on and create our user model with the correct permissions so our users can only access and update their own data.

Creating the users model

In the GIF above, we set up our users model with the following fields:

  • id - This is the same id (uid) that Firebase assigns to our users when they sign up on our app. We will set this value when we sync our Firebase users on Hasura.
  • email - Similar to the id field, this comes from Firebase as well.
  • name - We want to know what to call our users, don't we?
  • created_at - Automatically set by Hasura when a user is added.
  • updated_at - Automatically set and updated by Hasura whenever a user object is updated.

There's a tiny bug here. When we create the name field for our users, we have to make it a nullable field as when we create our user object, our users won't have a name attached to them so if it isn't nullable, Hasura will throw an error. Fixing this is very simple, just go into the modify tab under the users' table and edit the name field and set nullable to true.

Now that we've created our users model, let's set up some permissions so our users can only view and update their own data.

Setting user permissions

In this GIF, we're configuring the permissions for our users. What we're doing here is allowing a user to only select or update the data if their ID is equal to the ID of the user they are selecting or updating the data for. Setting it this way makes sure that one user cannot modify or even read another user's data.

Another thing to note here is that we're allowing users to read the id, email, and the name but only allowing them to update the name as we want to restrict other updates to Firebase.

Configuring JWT Claims

Okay, so we just set up our permissions so users can read and update only their own data. But, how does Hasura know which user is making the incoming request? It identifies the user by parsing the JWT we make the request with. And to set the proper JWT claims on our user, we are going to make use of the Firebase admin SDK and cloud functions.

A small note before we use cloud functions. Our cloud functions will be making external requests and to do that, Firebase requires that we upgrade to the Blaze plan. We still won't be charged because the first 2 million requests (yes, 2 million) are free but we still need to upgrade before we can make external requests.

To start working with Firebase cloud functions, we first need to install firebase-tools.

npm install -g firebase-tools

Then, we need to initialize our Firebase project. I tend to set my projects up in a parent directory, so I have a directory called full-stack-react-native and within it, I have an app directory that contains my Expo app. So in that parent directory, I will initialize Firebase.

You may need to log in to the CLI before running the following commands. To check if you're logged in, simply run firebase login. And once you're logged in, just run firebase-init to set up your project.

firebase init

You can see how I initialized my project in the GIF above but once NPM is done installing everything, it should create a functions directory for us. So let's move into that directory and start writing out cloud function.

const functions = require("firebase-functions");
const admin = require("firebase-admin");

admin.initializeApp(functions.config().firebase);

exports.registerUser = functions.https.onCall(async (data) => {
  const { email, password } = data;

  if (email === null || password === null) {
    // We are throwing an error if either the email or the password is missing
    // We should also ideally validate these on the frontend so the request is never made if those fields are missing
    throw new functions.https.HttpsError("invalid-argument", "email and password are required fields");
  }

  try {
    // We create our user using the firebase admin sdk
    const userRecord = await admin.auth().createUser({ email, password });

    // We set our user role and the x-hasura-user-id claims
    // Remember, the x-hasura-user-id is what Hasura uses to check
    // if the user is allow to read/update a record
    const customClaims = {
      "https://hasura.io/jwt/claims": {
        "x-hasura-default-role": "user",
        "x-hasura-allowed-roles": ["user"],
        "x-hasura-user-id": userRecord.uid
      }
    };

    await admin.auth().setCustomUserClaims(userRecord.uid, customClaims);
    return userRecord.toJSON();

  } catch (e) {
    let errorCode = "unknown";
    let msg = "Something went wrong, please try again later";
    if (e.code === "auth/email-already-exists") {
      // If a user that already has an account tries to sign up
      // we want to show them a proper error and instruct them to log in
      errorCode = "already-exists";
      msg = e.message;
    }
    throw new functions.https.HttpsError(errorCode, msg, JSON.stringify(e) );
  }
});
index.js

The code above is how we will be creating our users from here on in. How this will work is:

  • Our app will call our Firebase cloud function when a user tries to sign up
  • The cloud function will then create a new user using the admin SDK
  • Once the user is created, it will then set custom claims so our user can access our database
  • It will return the created user (or any errors) to notify our app upon completion

To push our cloud function to the cloud (hehe), go back to the parent directory and run firebase deploy --only functions.

So now that we've written and deployed our cloud function to register our users and assign the correct JWT claims, we need to instruct our app to use it. To do that, we need to update our Signup.js component to use cloud function instead. And to allow it to use the cloud function, we need to update our firebase config to allow us to use those cloud functions. I know it sounds complicated but it's easy enough once you actually do it.

Open your lib/firebase.js file and add the following lines to it.

import 'firebase/functions';

export const fns = firebase.functions();
firebase.js

All we're doing here is importing the firebase/functions package and exporting it as a variable so we can use it in our app.

Now, go to your screens/Signup.js file and make the following changes.

- import Firebase from '../lib/firebase';
+ import Firebase, { fns } from '../lib/firebase';

- await Firebase.auth().createUserWithEmailAndPassword(email, password);
// Extract the function into a variable
const registerUser = fns.httpsCallable('registerUser');

// Call the function
await registerUser({ email, password });

// Log the user in
await Firebase.auth().signInWithEmailAndPassword(email, password);
Signup.js

Once we make these changes and try signing up again, it should still work like it was earlier but this time, it is using our cloud function (check the logs) and our users have the correct permissions! You can't see it anywhere so you're just going to have to take my word for it at this point.

Syncing Firebase users with Hasura

Okay, so our users are now signing up with proper permissions but they still aren't on Hasura. Let's fix that.

To configure the syncing, we're going to set up two more cloud functions. One for when a user signs up and one for when a user deletes their account but before we do that, let's install a library that allows us to make requests to Hasura.

npm install graphql-request

Once NPM is done taking its own good time, add the following code to your functions/index.js file.

const request = require("graphql-request");

// Make sure you update the endpoint and the secret
// You can find both these values on the Graphiql tab in Hasura
const client = new request.GraphQLClient(YOUR_HASURA_API_ENDPOINT, {
  headers: {
    "content-type": "application/json",
    "x-hasura-admin-secret": YOUR_ADMIN_SECRET
  }
});

// This is automatically triggered by Firebase
// whenever a new user is created
exports.processSignUp = functions.auth.user().onCreate(async user => {
  const { uid: id, email } = user;
  const mutation = `
    mutation($id: String!, $email: String) {
      insert_users(objects: [{
        id: $id,
        email: $email,
      }]) {
        affected_rows
      }
    }
  `;
  
  try {
    const data = await client.request(mutation, { id, email });

    return data;
  } catch (e) {
    throw new functions.https.HttpsError('invalid-argument', e.message);
  }
});

// This again is automatically triggered
// whenever an account is deleted on Firebase
exports.processDelete = functions.auth.user().onDelete(async (user) => {
  const mutation = `
    mutation($id: String!) {
      delete_users(where: {id: {_eq: $id}}) {
        affected_rows
      }
    }
  `;
  const id = user.uid;

  try {
    const data = await client.request(mutation, {
      id: id,
    })
    return data;
  } catch (e) {
    throw new functions.https.HttpsError('invalid-argument', e.message);
  }
});
index.js

In the snippet above, we import and configure our GraphQL client with admin privileges and set up two functions. Both these functions are automatically called by Firebase, you can read more about them here. Basically, when a user signs up, we want to create that user on Hasura, and when a user deletes their account, we want to remove them from Hasura. This way, everything stays in sync!

Once you add these functions, don't forget to deploy them by running firebase deploy --only functions.

Hasura Firebase sync

Setting up GraphQL

So our users can now sign up to our app with the right permissions and when they do so, they are being created properly on both Firebase and Hasura. Now, let's make some use of those permissions and allow our users to update their name. To do this, we need to interact with our Hasura database, and to do that, we are going to use Apollo.

If you're not familiar with GraphQL or Apollo, the amazing people at Hasura have a really good guide here to help you get started. You can learn all about queries, mutations, subscriptions, and even optimistic updates. My only issue with the guide is the way they set up Apollo. Everything works but they don't account for the fact that your token will expire and once it does, your app will throw errors, and to take care of that, we're going to do things slightly differently.

Let's start by installing all the libraries we're going to use with Apollo.

yarn add @apollo/client @apollo/link-context graphql

Next, let's create lib/apollo.js where we will configure our Apollo client.

import { ApolloClient, HttpLink, InMemoryCache } from '@apollo/client';
import { setContext } from '@apollo/link-context';
// Make sure you create a new environment variable
// that points to your graphql endpoint
import { GRAPHQL_API_URL } from 'react-native-dotenv';

import Firebase from './firebase';

// This is where the magic happens, this function
// is called every time we make a request to our Hasura
// database. And because of that, we always get a fresh
// token. This way, we never run into issues with expired tokens
const asyncAuthLink = setContext(async () => {
  return {
    headers: {
      Authorization: `Bearer ${await Firebase.auth().currentUser.getIdToken()}`,
    },
  };
});

const httpLink = new HttpLink({
  uri: GRAPHQL_API_URL,
});

export const apolloClient = new ApolloClient({
  cache: new InMemoryCache(),
  link: asyncAuthLink.concat(httpLink),
});
apollo.js

Now that we've configured Apollo, let's use it in our app. Open the App.js component and add/modify these lines.

...
import { ApolloProvider } from '@apollo/client';

import { apolloClient } from './lib/apollo';
...

const App = () => {
  ...
  
  return (
  	...
      {user && (
        <ApolloProvider client={apolloClient}>
          <RootStack.Navigator
            screenOptions={{ headerShown: false }}
          >
            <RootStack.Screen name="Home" component={Home} />
          </RootStack.Navigator>
        </ApolloProvider>
      )}
    ...
  )
}

We're finally ready to use our Hasura database inside our app. Let's set up our Home component so that it asks our users to add their name and displays it once they add it.

To fetch and update our users data, we're going to use queries and mutations. So let's set those up before we edit our Home component. Let's create a file called lib/queries.js. This will be the place we store all our queries.

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

// We pass the user id and fetch the name
export const GET_USER = gql`
  query($id: String!) {
    users_by_pk(id: $id) {
      name
    }
  }
`;

// We pass the user id and the name in order to update it
export const UPDATE_USER_NAME = gql`
  mutation($id: String!, $name: String!) {
    update_users_by_pk(pk_columns: {id: $id}, _set: {name: $name}) {
      name
    }
  }
`;
queries.js

Okay, so our queries are ready, let's go ahead and use them in our Home component.

import React, { useState, useEffect } from 'react';
import { SafeAreaView, Text, TouchableOpacity, ActivityIndicator, View, TextInput } from 'react-native';
import tailwind, { getColor } from 'tailwind-rn';
import { useQuery, useMutation } from '@apollo/client';
import { Feather } from '@expo/vector-icons';

import Firebase from '../lib/firebase';
import { GET_USER, UPDATE_USER_NAME } from '../lib/queries';

const Home = () => {
  const id = Firebase.auth().currentUser.uid;
  const [editing, setEditing] = useState(false);
  const [name, setName] = useState('');
  const { loading: fetchLoading, data } = useQuery(GET_USER, { variables: { id }});
  const [ updateUser, { loading: mutationLoading, data: response } ] = useMutation(UPDATE_USER_NAME);
  useEffect(() => {
    let newName = null;

    if (data) {
      const { users_by_pk: user } = data;
      const { name } = user;
      {/* if a name already exists, set it as the name */}
      newName = name;
    }

    if (response) {
      const { update_users_by_pk } = response;
      const { name } = update_users_by_pk;
      {/* when a user updates their name, set that as the name */}
      newName = name;
    }

    setName(newName);
    {/* call this effect whenever data or response changes */}
  }, [data, response]);

  {/* Show a spinner if we are fetching/updating the data */}
  if (fetchLoading || mutationLoading) return (
    <SafeAreaView style={tailwind('flex-1 justify-center items-center')}>
      <ActivityIndicator color={getColor('blue-500')} />
    </SafeAreaView>
  )
  return (
    <SafeAreaView style={tailwind('flex-1')}>
      <View style={tailwind('py-10 px-5')}>
        <View style={tailwind('flex-row justify-between items-center')}>
          <Text style={tailwind('text-4xl font-bold')}>
            Home
          </Text>
          <TouchableOpacity
            style={tailwind('rounded-full bg-red-500 h-10 w-10 items-center justify-center')}
            {/* Log out function */}
            onPress={() => Firebase.auth().signOut()}
          >
            <Feather name='log-out' color='#fff' size={20} />
          </TouchableOpacity>
        </View>
        <View style={tailwind('mt-10')}>
          {/* If we don't have a name or
          the user wants to edit their name
          show the edit form */}
          {(!name || editing) && (
            <View>
              <Text style={tailwind('text-xl')}>
                What is your name?
              </Text>
              <View style={tailwind('border-b border-blue-500 mt-2 flex-row justify-between items-center pb-2')}>
                <TextInput
                  placeholder="Not Faraz"
                  onChangeText={val => setName(val)}
                  onFocus={() => setEditing(true)}
                  style={tailwind('text-2xl flex-grow')}
                />
                <TouchableOpacity
                  style={tailwind('bg-blue-500 h-8 w-8 rounded-full items-center justify-center')}
                  onPress={() => {
                    updateUser({
                      variables: {
                        id,
                        name
                      }
                    });
                    setEditing(false);
                  }}
                >
                  <Feather name='check' size={16} color='#fff' />
                </TouchableOpacity>
              </View>
            </View>
          )}
          {/* If we have a name, display it
          with an option to edit */}
          {(name && !editing) && (
            <View>
              <Text style={tailwind('text-xl')}>
                Did I get your name right?
              </Text>
              <View style={tailwind('mt-2 flex-row justify-between items-center pb-2')}>
                <Text
                  style={tailwind('text-2xl text-blue-500')}
                >
                  {name}
                </Text>
                <TouchableOpacity
                  style={tailwind('bg-blue-500 h-8 w-8 rounded-full items-center justify-center')}
                  onPress={() => setEditing(true)}
                >
                  <Feather name='edit' size={16} color='#fff' />
                </TouchableOpacity>
              </View>
            </View>
          )}
        </View>
      </View>
    </SafeAreaView>
  );
}

export default Home;
Home.js

So a lot is happening in the above snippet so I've tried to leave comments to explain things better. But the basic gist of things is, if we don't have a user's name, we ask them for it. If we do, we show it and also give them an option to edit their name if they choose to do so.

Query and Mutation demo

That was a lot, wasn't it? Let's take a step back and look at everything we did:

  • We set up Firebase auth and allowed our users to sign up and log into our app.
  • We created a Hasura instance, configured our users model, and its permissions.
  • We set up Firebase cloud functions to create our users with proper JWT claims and sync them with our Hasura database.
  • We set up Apollo on our Expo app to allow us to query and update our Hasura database and then we went ahead and fetched and updated our user's name.

You can try running the demo app on your Expo client here. I have also uploaded all the code in two separate repositories. The app itself can be found in this repo and the cloud functions can be found here.

As usual, feel free to reach out to me on Twitter if you have any questions or ideas/suggestions on how to do things better. Also, this blogging thing? It is kind of growing on me so if you have any ideas for posts you'd like to see, let me know!

Tags:

Faraz Patankar

Probably obsessing over fantasy football, figuring out what to eat for my next meal or working on my next great (soon to be abandoned) side project but hello to you too! 🤗

More posts from this author

Great! You've successfully subscribed.
Great! Next, complete checkout for full access.
Welcome back! You've successfully signed in.
Success! Your account is fully activated, you now have access to all content.