Using Context API & ReactFire with Ionic Framework & Capacitor

Aaron K Saunders - Aug 5 '20 - - Dev Community

This is a continuation of a series of blog post showing Firebase ReactFire in action with Ionic Framework React Components. In this post, we will move all of the data interaction with firebase into a separate component using Context API and React Hooks to separate Firebase specific code from the User Interface components of the application.

Setting Up The Context

Add the required imports to the file

// DataContext.tsx
import React from "react";
import { useFirebaseApp, useFirestoreCollectionData } from "reactfire";
import { FIREBASE_COLLECTION_NAME } from "./env";

// type for accessing the data structure for saving in firebase
import { IModalData } from "./components/AddSomethingModal";
Enter fullscreen mode Exit fullscreen mode

Next we can describe the shape of the state as an interface that we will use when setting up the context, it will allow us to use intellisense and errors will get generated when compiling is parameters don't match what is expected

// DataContext.tsx
interface IState {
  dataCollection: null | undefined | any;
  addItem: (itemData: IModalData) => Promise<void>;
  removeItem: (itemData: IModalData) => Promise<void>;
}

// create the context
export const DataContext = React.createContext<IState | undefined>(undefined);
Enter fullscreen mode Exit fullscreen mode

Next create the context provider, we are using and use the state object to ensure that we get reactive values from the context...

export const DataProvider: React.FC = ({ children }) => {

  // the store object
  let state = { 
    // functions and properties associated with the context
    // are included as part of the state object here
  };

// wrap the app in the provider with the initialized context
  return <DataContext.Provider value={state}>{children}</DataContext.Provider>;
};
Enter fullscreen mode Exit fullscreen mode

Finally return the DataContext and then a helper function useDataProvider so we can access the context in the application when we need to

export default DataContext;
export const useDataProvider = () =>
  React.useContext<IState | undefined>(DataContext)!;
Enter fullscreen mode Exit fullscreen mode

Filling Out The Context We Created

We need to be able to access the data collection and manipulate the data collection from the context. This means the shape of out state object is as follows

// the store object
let state = {
  dataCollection: data,
  addItem,              // function, adds to collection
  removeItem,           // function, remove from collection
};
Enter fullscreen mode Exit fullscreen mode

and the function as are implemented as follows, using the firebase code that was previously in the UI components

 /**
  * @param itemData
  */
 const addItem = (itemData: IModalData) => {
   return thingsRef.doc().set({ ...itemData });
 };

 /**
  * @param itemData
  */
 const removeItem = (itemData: IModalData) => {
   return thingsRef.doc(itemData.id).delete();
 };
Enter fullscreen mode Exit fullscreen mode

Finally we use the reactFire hooks to get the data collection and to setup the collectionRef that we need for our functions above.

// another reactfire hook to get the firebase app
const thingsRef = useFirebaseApp()
  .firestore()
  .collection(FIREBASE_COLLECTION_NAME);

// another hook to query firebase collection using
// the reference you created above
const data = useFirestoreCollectionData(thingsRef, { idField: "id" });
Enter fullscreen mode Exit fullscreen mode

Using the DataContext In the App

We want to be specific where we wrap the app using the <DataProvider>, since we have separated out the public components, that is where we will start.

// App.tsx
const PrivateRoutes: React.FunctionComponent = () => {
  return (
    <IonRouterOutlet>
      <Route exact path="/home">
        <DataProvider>
          <Home />
        </DataProvider>
      </Route>
      <Redirect exact path="/" to="/home" />
    </IonRouterOutlet>
  );
};
Enter fullscreen mode Exit fullscreen mode

Now inside of the <Home /> we have access to the context information.

We start with getting the state information from the context using the helper function we provided

const { 
   addItem, 
   removeItem, 
   dataCollection 
} = useDataProvider();
Enter fullscreen mode Exit fullscreen mode

Removing An Item

The function utilizing the context information

/**
 * @param item IModalData
 */
const removeSomething = (item: IModalData) => {
  removeItem(item)
    .then(() => showAlert("Success"))
    .catch((error: any) => {
      showAlert(error.message, true);
    });
};
Enter fullscreen mode Exit fullscreen mode

In the render method we using the dataCollection property to access the list of objects and the removeSomething function to access the code to remove the item when list entry is clicked

<IonList>
  {dataCollection.map((e: any) => {
    return (
      <IonItem key={e.id} onClick={() => removeSomething(e)}>
        <IonLabel className="ion-text-wrap">
          <pre>{JSON.stringify(e, null, 2)}</pre>
        </IonLabel>
      </IonItem>
    );
  })}
</IonList>
Enter fullscreen mode Exit fullscreen mode

Adding An Item

The function that is utilizing the context information

/**
 * @param response IModalResponse
 */
const addSomething = async (response: IModalResponse) => {
  setShowModal(false);
  if (response.hasData) {
    alert(JSON.stringify(response.data));
    addItem(response.data!)
      .then(() => showAlert("Success"))
      .catch((error: any) => {
        showAlert(error.message, true);
      });
  } else {
    showAlert("User Cancelled", true);
  }
};
Enter fullscreen mode Exit fullscreen mode

Integration in the render method

{/* ionic modal component */}
<IonModal isOpen={showModal} onDidDismiss={() => setShowModal(false)}>
  {/* our custom modal content */}
  <AddSomethingModal
    onCloseModal={(data: IModalResponse) => addSomething(data)}
  />
</IonModal>
Enter fullscreen mode Exit fullscreen mode

Source Code

Project available on GitHub, please look for the specific tag associated with this blog post.

DataContext.tsx

import React from "react";
import { useFirebaseApp, useFirestoreCollectionData } from "reactfire";
import { FIREBASE_COLLECTION_NAME } from "./env";

import { IModalData } from "./components/AddSomethingModal";

interface IState {
  dataCollection: null | undefined | any;
  addItem: (itemData: IModalData) => Promise<void>;
  removeItem: (itemData: IModalData) => Promise<void>;
}

// create the context
export const DataContext = React.createContext<IState | undefined>(undefined);

// create the context provider, we are using use state to ensure that
// we get reactive values from the context...

export const DataProvider: React.FC = ({ children }) => {
  // another reactfire hook to get the firebase app
  const thingsRef = useFirebaseApp()
    .firestore()
    .collection(FIREBASE_COLLECTION_NAME);

  // another hook to query firebase collection using
  // the reference you created above
  const data = useFirestoreCollectionData(thingsRef, { idField: "id" });

  /**
   *
   * @param itemData
   */
  const addItem = (itemData: IModalData) => {
    return thingsRef.doc().set({ ...itemData });
  };

  /**
   *
   * @param itemData
   */
  const removeItem = (itemData: IModalData) => {
    return thingsRef.doc(itemData.id).delete();
  };

  // the store object
  let state = {
    dataCollection: data,
    addItem,
    removeItem,
  };

  // wrap the application in the provider with the initialized context
  return <DataContext.Provider value={state}>{children}</DataContext.Provider>;
};

export default DataContext;
export const useDataProvider = () =>
  React.useContext<IState | undefined>(DataContext)!;
Enter fullscreen mode Exit fullscreen mode

Home.tsx

import React, { useState } from "react";
import {
  IonPage,
  IonButtons,
  IonButton,
  IonHeader,
  IonToolbar,
  IonTitle,
  IonContent,
  IonLabel,
  IonLoading,
  IonList,
  IonItem,
  IonModal,
  IonAlert,
} from "@ionic/react";

import { useAuth, AuthCheck } from "reactfire";
import "firebase/firestore";
import AddSomethingModal, {
  IModalResponse,
  IModalData,
} from "../components/AddSomethingModal";
import { useHistory } from "react-router";
import { useDataProvider } from "../DataContext";

type IShowAlert = null | {
  header: string;
  subHeader: string;
  message: string;
};

const Home: React.FunctionComponent = () => {
  // reactfire hook to get auth information
  const auth = useAuth();
  const history = useHistory();
  const { addItem, removeItem, dataCollection } = useDataProvider();

  console.log(dataCollection);

  // manages the state to determine if we need to open
  // the modal or not
  const [showModal, setShowModal] = useState(false);

  // manages the state to determine if we need to open
  // the modal or not
  const [showErrorAlert, setShowErrorAlert] = useState<IShowAlert>(null);

  /**
   * call this function to set state to get the alert
   * to display
   *
   * @param message
   * @param isError
   */
  const showAlert = (message: string, isError: boolean = false) => {
    setShowErrorAlert({
      header: "App Alert",
      subHeader: isError ? "Error" : "Notification",
      message: message,
    });
  };

  /**
   *
   * @param item IModalData
   */
  const removeSomething = (item: IModalData) => {
    removeItem(item)
      .then(() => showAlert("Success"))
      .catch((error: any) => {
        showAlert(error.message, true);
      });
  };

  /**
   *
   * @param response IModalResponse
   */
  const addSomething = async (response: IModalResponse) => {
    setShowModal(false);
    if (response.hasData) {
      alert(JSON.stringify(response.data));
      addItem(response.data!)
        .then(() => showAlert("Success"))
        .catch((error: any) => {
          showAlert(error.message, true);
        });
    } else {
      showAlert("User Cancelled", true);
    }
  };

  return (
    <IonPage>
      <IonHeader>
        <IonToolbar color="light">
          <IonButtons slot="end">
            <IonButton
              onClick={() => {
                auth.signOut();
                history.replace("/login");
              }}
            >
              Logout
            </IonButton>
          </IonButtons>
          <IonTitle>Home</IonTitle>
        </IonToolbar>
      </IonHeader>
      <IonToolbar style={{ paddingLeft: 16, paddingRight: 16 }}>
        <IonButton
          title="Add Something"
          fill="outline"
          onClick={() => setShowModal(true)}
        >
          Add Something
        </IonButton>
      </IonToolbar>
      <IonContent className="ion-padding">
        {/**  Show Error when problem **/}
        <IonAlert
          isOpen={showErrorAlert !== null}
          onDidDismiss={() => setShowErrorAlert(null)}
          header={showErrorAlert?.header}
          subHeader={showErrorAlert?.subHeader}
          message={showErrorAlert?.message}
          buttons={["OK"]}
        />

        {/* ionic modal component */}
        <IonModal isOpen={showModal} onDidDismiss={() => setShowModal(false)}>
          {/* our custom modal content */}
          <AddSomethingModal
            onCloseModal={(data: IModalResponse) => addSomething(data)}
          />
        </IonModal>

        {/* auth check and loader while in progress */}
        <AuthCheck fallback={<IonLoading isOpen={true} />}>
          {/* list of items from reactfire */}
          <IonList>
            {dataCollection.map((e: any) => {
              return (
                <IonItem key={e.id} onClick={() => removeSomething(e)}>
                  <IonLabel className="ion-text-wrap">
                    <pre>{JSON.stringify(e, null, 2)}</pre>
                  </IonLabel>
                </IonItem>
              );
            })}
          </IonList>
        </AuthCheck>
      </IonContent>
    </IonPage>
  );
};
export default Home;

Enter fullscreen mode Exit fullscreen mode
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .