Ionic React + Firebase: React Hook Form

Get user input and put it into Firestore

Here is an example of a React Hook Form that I’ve built in an application, as well as some details about how the form works.

It’s hard to find more advanced examples of React Hook Forms so I added several moving parts to this form, including using Firestore and additional form packages.

I want to preface the code for this form by encouraging you to take this code and make it better! There are a ton of ways to make React Hook Forms more complex and useful.

Here is what the form looks like:

First, you’ll need to use a couple of useful packages:

In order for this code to work, you’d also need to have a project already up and running on Firebase, and you’ll also need to have Google validation set up.

You can enable Google as a Sign-in method in your Firebase project.

Install these packages

npm install --save react-firebase-hooks 
npm install react-hook-form

See Full Code At The Bottom

Imports

import { IonContent, IonHeader, IonPage, IonTitle, IonToolbar, IonGrid, IonCol, IonRow, IonLabel, IonInput, IonList, IonItem, IonButton, IonText, IonIcon, IonChip} from '@ionic/react';

Import some Ionic Components.

import React, { useState } from 'react';

Import useState for monitoring and changing state in React hooks.

import { useAuthState } from 'react-firebase-hooks/auth';

Import useAuthState for retrieving and using authentication in Firebase.

import firebase from 'firebase';

Import firebase.

import { useForm, Controller } from 'react-hook-form';

Import useForm and Controller which is the form that comes from React Hook Forms and a controller to handle the form fields, rules, validation, default values, and those sorts of things.

import { home, card, cash, person, chatbubbles } from 'ionicons/icons';

Import some Icons to make the form look a bit more pleasant and not so bland.

The Page Level ‘ProfileSetup’ Component

const ProfileSetup: React.FC = () => { const [user] = useAuthState(firebase.auth()); 
function createUserData(){ if (user){
firebase.firestore().collection('users').doc(user.uid).set({
name: "test-name",
}, { merge: true });
//Merge checks to see if values exist, if so don't overwrite everything, just update}} if (user){
console.log("creating user data");
createUserData();
}

The const [user] = useAuthState(firebase.auth()); hook manages the state of the user and allows access to some user properties such as:

The createUserData(){} function is only meant to run once since this form is only shown to new users when they create a new account.

Inside this function, we’re going to create a new document for a user with an ID of user.uid inside the 'users' collection in Firestore.

firebase.firestore().collection('users').doc(user.uid).set({ name: "test-name", }, { merge: true });

If the ‘users’ collection does not exist, it will be created. Likewise, if a document for the user does not exists, then create one and set the field name: "test-name".

The additional option {merge: true} essentially prevents data from being overwritten if the fields already exist within the document. Rather, it will just update the field or add new ones that do not exist.

Your Cloud Firestore should end up being structured like this:

This is a good way to store additional properties for users beyond the Google account properties from useAuthState. This also ensures that users all have a unique ID within the collection.

if (user){ console.log("creating user data"); createUserData(); }

Above, we’re just making sure to run createUserData() when the script executes.

Another good place to run this would be inside ionViewWillEnter in order to stay within the structure of the Ionic Page Life Cycle. This would look something like this:

ionViewWillEnter() {if (user){ 
console.log("creating user data");
createUserData();
}
}

Pretty simple. Either way works.

The updateUserData() function will update the database with the data from the form and will be executed on a successful form submit. The data comes in as a generic type data: any , but inside our firebase.firestore().collection('users').doc(user.uid).set({data} function we make the data a specific Number type.

function updateUserData(data: any){if (user){ 
console.log("setting up user data for user with UID: " + user.uid); console.log("cash: " + Number(data["cash"]));
firebase.firestore().collection('users').doc(user.uid).set({monthlyIncome: Number(data["monthlyIncome"]),
monthlyExpenses: Number(data["monthlyExpenses"]),
cash: Number(data["cash"]),
realEstate: Number(data["realEstate"]),
checkingAccounts: Number(data["checkingAccounts"]),
savingsAccounts: Number(data["savingsAccounts"]), retirementAccounts: Number(data["retirementAccounts"]),
otherAssets: Number(data["otherAssets"]),
mortgages: Number(data["mortgages"]),
consumerDebt: Number(data["consumerDebt"]),
personalLoans: Number(data["personalLoans"]),
studentLoans: Number(data["studentLoans"]),
otherDebts: Number(data["otherDebts"]),
}, { merge: true }) } console.log(JSON.stringify(data, null, 2));
}

Next, we need to bring in the form package useForm that we imported so that we can use all of it's functionality.

let initialValues = { 
monthlyIncome: 0.00,
monthlyExpenses: 0.00,
cash: 0.00,
realEstate: 0.00,
checkingAccounts: 0.00,
savingsAccounts: 0.00,
retirementAccounts: 0.00,
otherAssets: 0.00,
mortgages: 0.00,
consumerDebt: 0.00,
personalLoans: 0.00,
studentLoans: 0.00,
otherDebts: 0.00, };
const { control, handleSubmit, formState, reset, errors } = useForm({ defaultValues: { ...initialValues }, mode: "onChange" });

We set the initial values of the form fields to the initial values that we created.

const [data, setData] = useState(); const showError = (_fieldName: string) => { 
let error = (errors as any)[_fieldName];
return error ? (
<div style={{ color: "red", fontWeight: "bold" }}>
{error.message || "Field Is Required And Must Be In Valid Dollar Format (00.00)"} </div> ) : null;
};

const [data, setData] = useState(); lets us reassign new values to the data variable and manage it's state.

If the current input in the form field is invalid, we can show an error below the field with showError. You'll see below how we display the error message within the document.

const onSubmit = (data: any) => { console.log(data); console.log(JSON.stringify(data, null, 2)); setData(data); updateUserData(data); };

This onSubmit function is going to be called upon a successful submit and log the data to the console as well as update the database with the valid data from the form by calling the updateUserData(data) function.

Next we construct what we want the profile setup form page by setting up an IonToolBar with some fancy Ionic components like IonChip , IonLabel , IonIcon and two different IonTitle components. This is wrapped in the IonHeader and at the top of the IonPage.

return ( 
<IonPage>
<IonHeader>
<IonToolbar>
<IonChip slot="start" outline color="dark"><IonLabel><IonTitle color="dark" size="large">Virta</IonTitle></IonLabel><IonIcon icon={person}></IonIcon></IonChip> <IonTitle size="small" slot="end" color="dark">Finance Companion</IonTitle> </IonToolbar>
</IonHeader>

Next, the IonContent holds the pretty text, ion chips, ion labels, and ion texts in columns and rows within the grid.

<IonContent> 
<IonGrid>
<IonRow>
<IonCol text-center>
<IonChip>
<IonLabel> <p><strong>Welcome to <IonText color="tertiary">Virta</IonText>, {user && user.displayName}.</strong></p> </IonLabel>
</IonChip>
<br></br>
<br></br>
<IonChip>
<IonLabel>
<IonIcon color="tertiary" icon={person}></IonIcon>
<IonIcon color="tertiary" icon={chatbubbles}></IonIcon>
</IonLabel>
</IonChip>
<IonLabel> <p><strong> Before you start improving your finances, I need to collect some basic information about your finances to set up your profile and calculate your net worth. </strong></p> </IonLabel> </IonCol>
</IonRow>

Now we make an IonList that holds our form.

<IonList> 
<IonItem> <IonButton routerLink="/home" routerDirection="forward" color="warning">Go Back</IonButton> </IonItem>
<form onSubmit={handleSubmit(onSubmit)} style={{ padding: 0 }}> <IonItem>
<IonText color="tertiary"><h2>Income/Expenses</h2></IonText> <IonIcon icon={cash} slot="end" color="tertiary"></IonIcon> </IonItem>
<IonItem>
<IonLabel>Monthly Income ($): </IonLabel>
<Controller
as={ <IonInput placeholder="0.00" clearInput> </IonInput> }
control={control}
onChangeName="onIonChange"
onChange={([selected]) => {
console.log("cash", selected.detail.value);
return selected.detail.value;
}}
name="monthlyIncome"
rules={{
required: true,
pattern: new RegExp('^[0-9]+(\.[0-9]{1,2})?$'),
}} />
</IonItem>
{showError("monthlyIncome")}

You can see the first input item <IonItem> in the form that has access to onChange which currently just logs the value of the input to the console. There are also two validation rules which are:

And then below the item we show any error messages with {showError("monthlyIncome")}

Below in the Full Code, you can see the rest of the items in the form which are nearly the same as the first "monthlyIncome" item. Also have a look below the last "otherDebts" item in the form at the line:

We keep the submit button disabled until a valid for submit occurs.

See Full Code

Originally published at https://austinhoward.tech on April 22, 2020.

Software Engineer & Writer.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store