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;
Top comments (0)