Using Context API & ReactFire with Ionic Framework & Capacitor
Tags: firebase, javascript, react, ionic
Published: 2020-08-05 00:48:46

This is a continuation of a series of blog post showing [Firebase ReactFire](https://github.com/FirebaseExtended/reactfire) in action with [Ionic Framework React Components](https://ionicframework.com/docs/react). In this post, we will move all of the data interaction with firebase into a separate component using [Context API](https://reactjs.org/docs/context.html) and [React Hooks](https://reactjs.org/docs/hooks-reference.html#usecontext) to separate Firebase specific code from the User Interface components of the application.

### Setting Up The Context
Add the required imports to the file
```react
// 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
```react
// 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...
```react
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
```react
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
```react
// 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
```react
 /**
  * @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.
```react
// 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.
```react
// 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
```react
const { 
   addItem, 
   removeItem, 
   dataCollection 
} = useDataProvider();
```
#### Removing An Item
The function utilizing the context information
```react
/**
 * @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
```react
<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
```react
/**
 * @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
```react
{/* 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.
- [Latest Code On HEAD](https://github.com/aaronksaunders/ionic-react-hook-form-react-fire)
- [Release Specific To This Blog Post - v1.3](https://github.com/aaronksaunders/ionic-react-hook-form-react-fire/tree/v1.3)


`DataContext.tsx`
```react
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`
```react
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;

```