How to Use Formik and Context API to Build Crowdbotics React Native Apps with Firebase

Generally, in React or React Native apps, data will be passed from parent to child via props, but this can increase the code base's complexity as certain props are required by many components within the application. The React Context API enables you to avoid passing props from parent to child at every level of the component tree. It also helps you avoid unnecessarily increasing the complexity of the codebase using state management libraries like Redux.

Consuming something like Firebase authentication and storage services with the Context API in React Native or Expo apps is a great use case for the Context API. In this tutorial, you will use a Crowdbotics-generated React Native app to learn how to:

  • add Context API to manage Firebase authentication
  • integrate Formik in already existing Forms
  • Validate forms using yup
  • handle real-time server errors
  • upgrade existing react-navigation flow to the latest version of 4.x.x

To get started, you are required to generate or have a React Native app generated using the Crowdbotics App Builder.

Upgrading Crowdbotics React Native app to latest Expo SDK

To setup a new project on the Crowdbotics App Builder, visit this link and create a new account. Once you have an individual account, access the app building platform with those credentials, and select "Create App" from the dashboard screen.

Crowdbotics dashboard screen

Select the Unsupported section as shown above, choose the Expo template, and click on the button Create App. This template lets you create a new project that can be highly customizable and useful if you are looking to build something of your own from scratch. The GitHub repository will take a couple of moments to generate.

React Native Expo Icon in Crowdbotics

Once the GitHub repo is ready, you will be able to either download or clone that GitHub repository to your local development environment. After you have cloned the repository, navigate inside the frontend directory and open the app.json file to change sdkVersion as below.

"sdkVersion": "35.0.0",

Next, open to package.json file and make the following changes.

"expo": "^35.0.0",
"react": "16.8.3",
"react-native": "https://github.com/expo/react-native/archive/sdk-35.0.0.tar.gz",
"react-navigation": "^4.0.6",
"@expo/vector-icons": "^10.0.0",

Now, open the terminal window and execute the command to install the dependencies.

# make sure to remove any package-lock.json or yarn.lock file first
rm -rf yarn.lock

# then install dependencies
yarn install

That's it. Expo SDK is quite easy to upgrade to the latest SDK. You won't be able to run it correctly since the react-navigation library in its latest version (4.x.x) requires an upgrade itself.

Upgrading react-navigation

Each navigational pattern (whether tab, stack, or drawer) for react-navigation has been modularized and separated as a separate dependency.

  • stack navigation, then install react-navigation-stack
  • for tabs install react-navigation-tabs
  • for drawer install react-navigation-drawer
  • switch navigation pattern is still under react-navigation and is only used for specific use cases such as authentication flow

You can find the navigational patterns being currently used by this Expo app in the src/navigators file. There are three files in this directory out of which only two navigation patterns are used:

  • stack
  • switch

To proceed, you will have to install the following dependencies first.

yarn add react-navigation-stack

expo install react-native-gesture-handler react-native-screens

Now, go to AuthNavigator.js. You are not going to modify the navigator pattern. Instead, just replace the statement to import createStackNavigator with the following line.

import { createStackNavigator } from 'react-navigation-stack'

Similarly, open the HomeNavigator.js file and do the same as above. There is no requirement to change the current code for the Switch navigation pattern, so the code snippet inside the AppNavigator.js file remains the same.

From the terminal window, run the expo start command to see the default Crowdbotics Expo Started Template in action. Make sure, depending on your development environment, that you have either a real device or an iOS or an Android simulator running. You will be welcomed by the following login screen.

Default Crowdbotics Expo login screen

Add Firebase Config

If you already know how to obtain the Firebase API and storage keys, you can skip this section. Otherwise, you can follow along. The initial step to start is to create a new Firebase project from Firebase Console.

New project icon in Firebase Console

Next, fill in the suitable details regarding the Firebase project and click on Create project.

You will be redirected to the dashboard of the Firebase project. Go to Project settings from the sidebar menu and copy the firebaseConfig object. It has all the necessary API keys that we need in order to use a Firebase project as the backend for any React Native or Expo app.

Firebase config object screen

Next, navigate inside the src and create a new directory called config. This folder will contain all the configuration files. Inside it, create firebaseConfig.js and paste the contents of the config object as below.

// Replace all Xs with real Firebase API keys
export default {
  apiKey: 'XXXX',
  authDomain: 'XXXX',
  databaseURL: 'XXXX',
  projectId: 'XXXX',
  storageBucket: 'XXXX',
  messagingSenderId: 'XXXX',
  appId: 'XXXX'
}

Next, from the terminal window, install the Firebase SDK.

yarn add firebase

Initialize Firebase app

Back to the config/ directory. Create a new file firebase.js. This will hold all the configurations related to integrate the Firebase SDK and the function it provides for authentication, real-time database, and so on.

Also, define a Firebase object with some initial methods that you are going to use in the tutorial. These methods are going to conduct real-time events such as user authentication, sign out from the app, and store the user details based on the reference to uid (unique user-id Firebase creates for every registered user) in a real-time NoSQL database called Cloud Firestore.

import * as firebase from 'firebase'
import 'firebase/auth'
import 'firebase/firestore'
import firebaseConfig from './firebaseConfig'

// Initialize Firebase
firebase.initializeApp(firebaseConfig)
const Firebase = {
  // auth
  loginWithEmail: (email, password) => {
    return firebase.auth().signInWithEmailAndPassword(email, password)
  },
  signupWithEmail: (email, password) => {
    return firebase.auth().createUserWithEmailAndPassword(email, password)
  },
  signOut: () => {
    return firebase.auth().signOut()
  },
  checkUserAuth: user => {
    return firebase.auth().onAuthStateChanged(user)
  },
  // firestore
  createNewUser: userData => {
    return firebase
      .firestore()
      .collection('users')
      .doc(`${userData.uid}`)
      .set(userData)
  }
}
export default Firebase

This approach used with Context API is going to eliminate the use of Redux as the state management library (which is the approach I worked with previously). Populating the Firebase object with Context API, you will be able to access all the functions as well as the user throughout this React Native app as props.

Enable Firestore

Before you proceed to dive deep into Context API, let's complete the setup of the Firestore as well. It follows proper NoSQL terminology when it comes to storing data. It stores data in documents, and each document can have sub-collections—thus, making it suitable for scalable and complex data scenarios.

Go back to the Firebase console and in the Database section, choose the Cloud Firestore and click on the button Create database.

Create database button in Firestore

Then, choose the option Start in test mode and click the button Next as shown below.

Create database settings screen in Firestore

That's it to enable Firestore.

Add Context API

As the official React documentation states about Context API:

Context is designed to share data that can be considered “global” for a tree of React components, such as the current authenticated user, theme, or preferred language.

The Context API consists of three building blocks:

  • creating a context object
  • declaring a provider that gives the value
  • declaring a consumer that allows a value to be consumed (provided by the provider)

Create a new file inside the config/ directory called context.js. Declare an object called FirebaseContext.

import React, { createContext } from 'react'

const FirebaseContext = createContext({})

After creating the context, the next step is to declare a provider and a consumer.

export const FirebaseProvider = FirebaseContext.Provider
export const FirebaseConsumer = FirebaseContext.Consumer

Lastly, declare an HoC (Higher-Order Component) to generalize this Firebase Context. An HoC in React is a function that takes a component and returns another component. Instead of importing and using Firebase.Consumer in every component where it is necessary, all you need to do is just pass the component as the argument to the following HoC.

export const withFirebaseHOC = Component => props => (
  <FirebaseConsumer>
    {state => <Component {...props} firebase={state} />}
  </FirebaseConsumer>
)

Things will be more clear in the next section when modifying the existing Login and Signup component with this HoC. Now, create a new file index.js to export the Firebase object from the firebase.js, the provider, and the HoC.

import Firebase from './firebase'
import { FirebaseProvider, withFirebaseHOC } from './context'

export default Firebase
export { FirebaseProvider, withFirebaseHOC }

Open the App.js file. The Provider will grab the value from the context object for the FirebaseConsumer to use that value. The value for the FirebaseProvider is going to be the Firebase object with different strategies and functions to authenticate and store the user data in the real-time database (defined in firebase.js file). Wrap the Redux store and the navigation container with FirebaseProvider.

// import from config directory
import Firebase, { FirebaseProvider } from './src/config'

// modify App component
class App extends Component {
  render() {
    return (
      <FirebaseProvider value={Firebase}>
        <Provider store={store}>
          <StatusBar barStyle='light-content' />
          <AppNavigator />
        </Provider>
      </FirebaseProvider>
    )
  }
}

That's it for setting up the Firebase SDK.

Add Formik to Signup Form

To proceed and modify existing the Login and Signup form components, install the following dependencies.

yarn add formik yup

Formik and yup are great development tools to build awesome-looking UI forms for your React Native application needs. Formik is a small library that helps organize forms in React and React Native with the following things:

  • it keeps track of form's state
  • handles form submission via reusable methods and handlers (such as handleChange, handleBlur, and handleSubmit)
  • handles validation and error messages out of the box

At times, it becomes hard to manage and fulfill the above points. Using Formik, you can understand what exactly is happening in forms and write fewer lines of code.

Let us modify the Signup form. Open src/containers/Signup/index.js. Import Formik library.

import { Formik } from 'formik'

Right now, the Signup form contains a component state object that has three fields: email, password and confirmPassword. Also, there is no business logic to validate whether the input of password and confirmPassword matches or not. Using Formik and yup, you are going to add business logic to make it work.

Inside the render function, wrap the Content inside the Formik wrapper element. It comes with different props to handle forms such as initialValues and onSubmit handler methods. The initialValues accepts an object containing form values. In the case of the current form, these values are going to be email, password, and confirmPassword.

The onSubmit method accepts a function that has these values as the first argument to handle the form submission.

class Signup extends Component {
  // navigate to login screen after a successful signup
  onSignupButtonPressed = () => {
    // TODO: Login

    this.props.navigation.navigate('Login')
  }

  // navigate to login screen
  onLoginButtonPressed = () => {
    this.props.navigation.navigate('Login')
  }

  render() {
    return (
      <Container style={styles.container}>
        <Formik
          initialValues={{ email: '', password: '', confirmPassword: '' }}
          onSubmit={values => {
            alert(JSON.stringify(values))
          }}>
          {({ handleChange, values, handleSubmit }) => (
            <Content contentContainerStyle={styles.content}>
              {/* Logo */}
              <View style={styles.logoContainer}>
                <Image
                  style={styles.logo}
                  source={require('../../assets/images/icon.png')}
                />
                <Text style={styles.logoText}>Crowdbotics</Text>
              </View>

              {/* Form */}

              <Form style={styles.form}>
                <Item style={styles.item} rounded last>
                  <Input
                    style={styles.input}
                    placeholder='Username'
                    placeholderTextColor='#afb0d1'
                    autoCapitalize='none'
                    value={values.email}
                    onChangeText={handleChange('email')}
                  />
                </Item>
                <Item style={styles.item} rounded last>
                  <Input
                    style={styles.input}
                    placeholder='Password'
                    placeholderTextColor='#afb0d1'
                    value={values.password}
                    onChangeText={handleChange('password')}
                    secureTextEntry
                  />
                </Item>
                <Item style={styles.item} rounded last>
                  <Input
                    style={styles.input}
                    placeholder='Confirm Password'
                    placeholderTextColor='#afb0d1'
                    value={values.confirmPassword}
                    onChangeText={handleChange('confirmPassword')}
                    secureTextEntry
                  />
                </Item>
              </Form>
              <View style={styles.buttonContainer}>
                <Button
                  style={styles.button}
                  onPress={handleSubmit}
                  hasText
                  block
                  large
                  dark
                  rounded>
                  <Text style={styles.signupText}>SIGNUP</Text>
                </Button>

                {/* Signup Button */}
                <View style={styles.loginContainer}>
                  <Text style={styles.haveAccountText}>
                    Already have an account?
                  </Text>
                  <TouchableOpacity onPress={this.onLoginButtonPressed}>
                    <Text style={styles.loginText}>Login Now.</Text>
                  </TouchableOpacity>
                </View>
              </View>
            </Content>
          )}
        </Formik>
      </Container>
    )
  }
}

For now, to see that the values of all input fields are being recorded, there is an alert method on the onSubmit prop. Try to fill in the input fields at the Signup form and an alert box will appear with the exact same value as the input you entered.

Alert prompt appears containing the sample login info

Add Formik to Login Form

Similar to the last section, here are the complete changes made to src/containers/Login/index.js, which contains the Login form component.

class Login extends Component {
  // navigate to home after a successful login
  onLoginButtonPressed = () => {
    // TODO: Login

    this.props.navigation.navigate('Home')
  }

  // navigate to signup screen
  onSignupButtonPressed = () => {
    this.props.navigation.navigate('Signup')
  }

  // navigate to forgot password screen
  onForgotPasswordButtonPressed = () => {
    this.props.navigation.navigate('ForgotPassword')
  }

  render() {
    return (
      <Container style={styles.container}>
        <Formik
          initialValues={{ email: '', password: '' }}
          onSubmit={values => {
            alert(JSON.stringify(values))
          }}>
          {({ handleChange, values, handleSubmit }) => (
            <Content contentContainerStyle={styles.content}>
              {/* Logo */}
              <View style={styles.logoContainer}>
                <Image
                  style={styles.logo}
                  source={require('../../assets/images/icon.png')}
                />
                <Text style={styles.logoText}>Crowdbotics</Text>
              </View>

              {/* Form */}
              <Form style={styles.form}>
                <Item style={styles.item} rounded last>
                  <Input
                    style={styles.input}
                    placeholder='Username/Email'
                    placeholderTextColor='#afb0d1'
                    autoCapitalize='none'
                    value={values.email}
                    onChangeText={handleChange('email')}
                  />
                </Item>
                <Item style={styles.item} rounded last>
                  <Input
                    style={styles.input}
                    placeholder='Password'
                    placeholderTextColor='#afb0d1'
                    value={values.password}
                    onChangeText={handleChange('password')}
                    secureTextEntry
                  />
                </Item>
              </Form>

              <View style={styles.buttonContainer}>
                {/* Login Button */}
                <Button
                  style={styles.button}
                  onPress={handleSubmit}
                  hasText
                  block
                  large
                  dark
                  rounded>
                  <Text style={styles.loginText}>LOGIN</Text>
                </Button>

                <View style={styles.forgotPasswordContainer}>
                  <TouchableOpacity
                    onPress={this.onForgotPasswordButtonPressed}>
                    <Text style={styles.forgotPasswordText}>
                      Forgot Password?
                    </Text>
                  </TouchableOpacity>
                </View>

                {/* Signup Button */}
                <View style={styles.signupContainer}>
                  <Text style={styles.dontHaveAccountText}>
                    Don't have an account?
                  </Text>
                  <TouchableOpacity onPress={this.onSignupButtonPressed}>
                    <Text style={styles.signupText}>Sign Up Now.</Text>
                  </TouchableOpacity>
                </View>
              </View>
            </Content>
          )}
        </Formik>
      </Container>
    )
  }
}

The output will be the same from the previous section. It will show the input fields entered in an alert box.

Validating form with yup

The yup library is useful to manage complex validation when using Formik in either React or React Native apps. Formik supports both synchronous and asynchronous form validation. It has support for schema-based form level validation from yup.

Open src/containers/Signup/index.js. Import everything from the yup library with other import statements.

import * as Yup from 'yup'

If you are familiar with Node.js development, you will find the yup library is quite similar to another validation library called joi. Next, let us define a new object before the Signup class component called validationSchema.

As initialValues is an object, you will have to specify yup.object() and define the shape of that object. When defining input fields inside an object as its shape, make sure their name corresponds to the same value described in initialValues.

Next, each field in this object is supported by a chain of validation methods provided by the yup API. All input types in the Signup form are going to be a string since the method onChangeText return values as strings.

const validationSchema = Yup.object().shape({
  email: Yup.string()
    .label('Email')
    .email('Enter a valid email')
    .required('Please enter a registered email'),
  password: Yup.string()
    .label('Password')
    .required()
    .min(6, 'Password should be at least 6 characters '),
  confirmPassword: Yup.string()
    .oneOf([Yup.ref('password')], 'Confirm Password must matched Password')
    .required('Confirm Password is required')
})

Using a library like Yup saves a lot of time, especially when you do not have to define custom validation methods to check for an input field. For example, in the above snippet, using .email() automatically matches against a regex instead of defining regex to check the validity of an email input field.

Also, for every valid method, you can enter a custom return message that's shown in case of an error. Look at the .required() line in the above code snippet. It's stating that when an email isn't provided, this message passed in quotes will be shown as the error message. Similarly, for password, when the length of the input field is less than four characters, it will display an error message. The last step for the validationSchema to work is to add a prop with the same name in the Formik element.

<Formik
  initialValues={{ email: '', password: '', confirmPassword: '' }}
  onSubmit={values => {
    alert(JSON.stringify(values))
  }}
  validationSchema={validationSchema}>
  {/* Rest of the code */}
</Formik>

Next, formikProps also provide errors to access error messages.

// pass errors below
{({ handleChange, values, handleSubmit, errors }) => (

After each input field, you will have to add a Text element to display the error message.

<Form style={styles.form}>
  <Item style={styles.item} rounded last>
    <Input
      style={styles.input}
      placeholder='Username/Email'
      placeholderTextColor='#afb0d1'
      autoCapitalize='none'
      value={values.email}
      onChangeText={handleChange('email')}
    />
  </Item>
  <Text style={{ color: 'red' }}>{errors.email}</Text>
  <Item style={styles.item} rounded last>
    <Input
      style={styles.input}
      placeholder='Password'
      placeholderTextColor='#afb0d1'
      value={values.password}
      onChangeText={handleChange('password')}
      secureTextEntry
    />
  </Item>
  <Text style={{ color: 'red' }}>{errors.password}</Text>
  <Item style={styles.item} rounded last>
    <Input
      style={styles.input}
      placeholder='Confirm Password'
      placeholderTextColor='#afb0d1'
      value={values.confirmPassword}
      onChangeText={handleChange('confirmPassword')}
      secureTextEntry
    />
  </Item>
  <Text style={{ color: 'red' }}>{errors.confirmPassword}</Text>
</Form>

Notice the error message displayed after each input field in the below demo.

Error messages appear upon invalid text entry

Show errors only if touch for a specific field

You must have noticed that the current state of the form shows errors for all fields even when the user is entering the first field and hasn't yet seen what is required in the second field.

To fix this, let us use touched and handleBlur from formikProps.

{({
	handleChange,
    values,
	handleSubmit,
	errors,
	touched,
	handleBlur
}) => ({/* ... rest of the code */})

The handleBlur is passed as the value to the onBlur prop on the input field. The prop is used to track whether an input field has been touched by the user or not. The touched tracks what fields have been touched. Using the combination of both, you can get the following behavior.

The error messages only show when fields have been left invalid

Here is the code snippet on how to do this. On each input field, add the onBlur prop with the corresponding value passed to the handleBlur method.

// for email
onBlur={handleBlur('email')}

// for password
onBlur={handleBlur('password')}

// for confirm password
onBlur={handleBlur('confirmPassword')}

Next, when displaying the error message, modify it is as follows for all fields.

// for email
<Text style={{ color: 'red' }}>
  {touched.email && errors.email}
</Text>

// for password
<Text style={{ color: 'red' }}>
  {touched.password && errors.password}
</Text>

// for confirm password
<Text style={{ color: 'red' }}>
  {touched.confirmPassword && errors.confirmPassword}
</Text>

Similarly, you can implement the previous two sections on the Login form as an extra challenge.

Email Authentication with Firebase

In this section, you are going to modify the existing Signup component in order to register a new user with the Firebase backend and store their data in Firestore. To start, import the withFirebaseHOC. Also, wrap the Signup component inside the withFirebaseHOC in the export statement:

// Signup/index.js
// after other imports
import { withFirebaseHOC } from '../../config'

// ... rest of the code

export default withFirebaseHOC(Signup)

Similarly, import the Firebase HoC component in the Login/index.js file.

// Login/index.js
// after other imports
import { withFirebaseHOC } from '../../config'

// ... rest of the code

export default withFirebaseHOC(Login)

The next step is to replace the alert in the onSubmit prop of the Formik element with the handler method onSignupButtonPressed.

onSubmit={values => {
  this.onSignupButtonPressed(values)
}}

The initialValues are being passed as a parameter to the handler method onSignupButtonPressed.

onSignupButtonPressed = async values => {
  const { email, password } = values
  try {
    const response = await this.props.firebase.signupWithEmail(email, password)
    if (response.user.uid) {
      const { uid } = response.user
      const userData = { email, uid }
      await this.props.firebase.createNewUser(userData)
      this.props.navigation.navigate('Home')
    }
  } catch (error) {
    console.error(error)
  }
}

The signupWithEmail is coming from Firebase props, and since you are already wrapping the navigation container with FirebaseProvider, this.props.firebase will make sure any method inside the Firebase object in the file config/firebase.js is available to be used in this component.

The signupWithEmail method takes two arguments, email and password. Using them, it creates a new user and saves their credentials. It then fetches the user id (uid) from the response when creating the new user. The createNewUser() method stores the user object userData inside the collection users in the Firestore. This user object contains the uid from the authentication response and email of the user entered in the signup form.

Let see how it works.

New account is successfully created

On successful signup, the user will be navigated to the Home screen. To verify this, visit the Database section from the Firebase Console dashboard.

The test user has been created in the Firebase database

Similarly, in the Login component, replace alert on the onSubmit prop of the Formik element with the handler method onLoginButtonPressed.

onSubmit={values => {
  this.onLoginButtonPressed(values)
}}

Next, modify the contents of the handler method.

onLoginButtonPressed = async values => {
  const { email, password } = values
  try {
    const response = await this.props.firebase.loginWithEmail(email, password)

    if (response.user) {
      this.props.navigation.navigate('Home')
    }
  } catch (error) {
    console.error(error)
  }
}

Now, try to login with the same credentials previously created.

Successful login

A successful login will lead the user to the Home screen.

Handle real-time server errors

Formik comes with a built-in solution to handle real-time server errors. Real-time server errors can vary. For example, when trying to login with an email that doesn't exist in the Firestore, the app should notify the user on the client-side by throwing an error.

To handle this, edit the onSubmit prop at the Formik element with passing another argument called actions. Open Login/index.js file.

onSubmit={(values, actions) => {
  this.onLoginButtonPressed(values, actions)
}}

Next, instead of just console logging the error values in the handler method onLoginButtonPressed, to display the error, you will have to use setFieldError. This will set an error message in the catch block. Also, add a finally block that will avoid the form to submit in case of an error.

onLoginButtonPressed = async (values, actions) => {
  const { email, password } = values
  try {
    const response = await this.props.firebase.loginWithEmail(email, password)

    if (response.user) {
      this.props.navigation.navigate('Home')
    }
  } catch (error) {
    actions.setFieldError('general', error.message)
  } finally {
    actions.setSubmitting(false)
  }
}

Lastly, to display the error on the app screen, add a Text element that contains the error just before the Button.

<Text style={{ color: 'red' }}>{errors.general}</Text>

Here is a short demo of real-time error when a user enters an email that does not exist in the Firebase.

Error appears upon invalid login submission

Conclusion

Congratulations! 🎉

If you have come this far, I hope you enjoyed reading this post. Formik and yup provide pragmatic solutions to build forms in React Native and Firebase applications. Context API also provides much more convenient access to props across the entire app.

Originally published:

May 13, 2020

Related Articles