DEV Community

loading...
Cover image for Using Context API & ReactFire with Ionic Framework & Capacitor

Using Context API & ReactFire with Ionic Framework & Capacitor

Aaron K Saunders
See more, like and subscribe 👉🏾 ‪Aaron Saunders 📺 https://www.youtube.com/aaronsaundersci?sub_confirmation=1
・5 min read

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";

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);

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>;
};

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)!;

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
};

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();
 };

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" });

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>
  );
};

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();

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);
    });
};

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>

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);
  }
};

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>

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)!;

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;

Discussion (0)