Firebase snapshot stored in React Context - reactjs

I'm starting to work on a brand new app and I have certain amount of data coming from Firestore that's always being used so I figured I'd just add it to React Context and make it available all the time (using a snapshot with real time updates). I've looked around and didn't find something similar so my question is, would this be a valid use case? Or should I be reading data each time a component needs it?
This would be my data.context.jsx file
import { collection, onSnapshot, query } from 'firebase/firestore';
import React, { createContext, useContext, useEffect, useState } from 'react';
import { AuthContext } from '../auth/auth.context';
import { FirebaseContext } from '../firebase/firebase.context';
export const DataContext = createContext();
export const DataContextProvider = ({ children }) => {
const { db } = useContext(FirebaseContext);
const { userId } = useContext(AuthContext);
const [loadingContactList, setLoadingContactList] = useState(false);
const [contactList, setContactList] = useState(null);
const [loadingNotesList, setLoadingNotesList] = useState(false);
const [notesList, setNotesList] = useState(null);
useEffect(() => {
if (!userId) return; // No userId, no business
setLoadingContactList(true);
console.log('Loading contact list');
const unsubscribe = onSnapshot(
query(collection(db, `users/${userId}/contacts`)),
snapshot => {
setContactList(
snapshot.docs.map(doc => {
return doc.data();
})
);
setLoadingContactList(false);
},
err => {
// TODO: handle errors;
}
);
return () => unsubscribe();
}, [userId, db, setContactList]);
useEffect(() => {
if (!userId) return; // No userId, no business
setLoadingNotesList(true);
console.log('Loading notes list');
const unsubscribe = onSnapshot(
query(collection(db, `users/${userId}/notes`)),
snapshot => {
setNotesList(
snapshot.docs.map(doc => {
return doc.data();
})
);
setLoadingNotesList(false);
},
err => {
// TODO: handle errors;
}
);
return () => unsubscribe();
}, [userId, db, setNotesList]);
return (
<DataContext.Provider
value={{
loading: loadingContactList || loadingNotesList,
loadingContactList,
loadingNotesList,
contactList,
notesList,
}}
>
{children}
</DataContext.Provider>
);
};
In another app I'm using swr-firestore (https://github.com/nandorojo/swr-firestore) which worked great cause it was holding a data cache but this library hasn't been updated in a long time.

Reviewing your project, using snapshot to get realtime updates with Cloud Firestore seems to be a valid use case.
Until the initial onSnapshot event resolves, the hook returns a loading state, which is true by default. It also generates a data array by looping through the documents returned by the snapshot. When an onSnapshot event happens, the re-render is triggered.
An initial call to the callback you give creates an instant document snapshot of the single document's present contents. The document snapshot is then updated every time the contents change.
An alternative solution could be React-Query is frequently referred to as React's missing data-requesting library, but in more technical terms, it makes fetching, caching, syncing, and updating server state in React apps a snap.
You can also review the React useContext Hook documentation.

Related

React useContext setState only updates first time

I'm currently building an app which allows a user to collect stamps by scanning a QR code. I have a Firebase Firestore snapshotlistener attached which listens for new stamps being added to that user. And rather than to fetch the new total amount of stamps for that user, I wanted to use the payload of the snapshot and add it to the list of stamps which is saved in a Context.
Unfortunately, this only works the first time when the app has been started. The procedure looks like this:
QR code gets scanned
New snapshot is received with newly added stamps (1 or more)
I'm logging the current state of total stamps to console, which looks OK.
The new stamps get added to the state context.
Logging the context state changes show the stamps were added correctly.
But the second time or any after that it looks like this:
QR code gets scanned
New snapshot is received with newly added stamps (1 or more)
I'm logging the current state of total stamps to console, but this still looks the same as before the first attempt.
The new stamps get added to the old state context.
Logging the context state changes show the stamps were added but the first was removed, so basically the first stamp has been overwritten.
You'd think this is simple, "just check you're dependency arrays". But this looks all okay to me.
Here's how my setup looks:
Context:
const AuthContextProvider = ({ children }: Props) => {
const [user, setUser] = useState<FirebaseAuthTypes.User>();
const [userData, setUserData] = useState<FirestoreUser>();
const [stamps, setStamps] = useState<GetMyStampsResponse>();
const [isLoading, setLoading] = useState(true);
// Unimportant for issue
const onAuthStateChanged = useCallback(
async (response: FirebaseAuthTypes.User | null) => {
...
},
[...]
);
useEffect(() => {
const unsubscribe = auth().onUserChanged(onAuthStateChanged);
return unsubscribe;
}, [onAuthStateChanged]);
return (
<AuthContext.Provider
value={{
isLoading,
stamps,
setStamps,
user,
userData,
reloadUser,
}}>
{children}
</AuthContext.Provider>
);
};
useAuth hook to be used in my components
import storage from '#react-native-firebase/storage';
import cloneDeep from 'lodash/cloneDeep';
import { useCallback, useContext } from 'react';
const useAuth = () => {
const { stamps, setStamps, reloadUser, userData, user, isLoading } =
useContext(AuthContext);
/**
* Add stamps that were received through a listener. This way we don't have to rely on reloadUser, which is more expensive.
*/
const addStampsFromListener = useCallback(
async (newStamps: FirestoreStamp[]) => {
let newStampsState = cloneDeep(stamps);
const newStampHasKnownStampcard = newStampsState?.cards.find(
c => c.ref.path === newStamps[0].stampcardRef.path,
);
console.log('current stamps state', newStampsState?.stamps.length);
if (newStampHasKnownStampcard) {
newStampsState = {
...newStampsState!,
stamps: newStampsState!.stamps.concat(newStamps),
};
} else {
const [stampcard, business] = await Promise.all([
stampcardApi.getStampcardByRef(newStamps[0].stampcardRef),
businessApi.getBusiness(newStamps[0].businessId),
]);
const imageUrl = await storage()
.ref(stampcard!.stampImage)
.getDownloadURL();
const stampcardWithRef: StampCardDataWithRef = {
...stampcard!,
imageUrl,
businessId: newStamps[0].businessId,
ref: newStamps[0].stampcardRef,
};
newStampsState = {
...newStampsState,
businesses: (newStampsState?.businesses ?? []).concat(business!),
cards: (newStampsState?.cards ?? []).concat(stampcardWithRef),
stamps: (newStampsState?.stamps ?? []).concat(newStamps),
};
}
setStamps(newStampsState);
return newStampsState;
},
[setStamps, stamps],
);
...
return {
addStampsFromListener,
updateStampsFromListener,
isLoading,
data: userData,
logout,
reloadUser,
stamps,
user,
};
};
And finally, the use of this context hook in my screen:
const HomeConsumerScreen = () => {
const {
stamps: stampcards,
reloadUser,
addStampsFromListener,
updateStampsFromListener,
} = useAuth();
const subscribeToStamps = useCallback(() => {
if (refIsSubscribedToStamps.current) return; // Prevent initialization when already running
const unsubscribe = stampApi.subscribeToStamps(
/**
* Callback for receiving new stamps
*/
querySnapshot => {
error && setError(undefined);
if (!refIsSubscribedToStamps.current) {
// First snapshot contains current state of all stamps belonging to a user
return;
}
const data = querySnapshot.map(doc =>
doc.doc.data(),
) as FirestoreStamp[];
addStampsFromListener(data).then(nextState =>
showDone('newStamp', data, nextState),
);
},
/**
* Initializer
*/
() => {
refIsSubscribedToStamps.current = true;
},
/**
* Error callback
*/
e => {
setError(e.message);
console.error('onSnapShot', e);
},
);
return unsubscribe;
}, [addStampsFromListener, error, showDone, updateStampsFromListener]);
useEffect(() => {
// Listen for changes to single stampcard
const unsubscribe = subscribeToStamps();
return () => {
unsubscribe && unsubscribe();
refIsSubscribedToStamps.current = false;
};
// Adding 'subscribeToStamps' to dependency hook will cause
// the listener to unmount&remount on every callback.
// It will fix the issue that I'm trying to solve here.... :(
}, []);
};
So it seems to revolve around adding subscribeToStamps to the useEffect dependency array, but that causes the listener to unmount&mount again which is just more costly.
Any advice?

Unsubscribe from listener in hook or in screen component?

I created a hook to manipulate users data and one function is listener for users collection.
In hook I created subscriber function and inside that hook I unsubscribed from it using useEffect.
My question is is this good thing or maybe unsubscriber should be inside screen component?
Does my approach has cons?
export function useUser {
let subscriber = () => {};
React.useEffect(() => {
return () => {
subscriber();
};
}, []);
const listenUsersCollection = () => {
subscriber = firestore().collection('users').onSnapshot(res => {...})
}
}
In screen component I have:
...
const {listenUsersCollection} = useUser();
React.useEffect(() => {
listenUsersCollection();
}, []);
What if I, by mistake, call the listenUsersCollection twice or more? Rare scenario, but your subscriber will be lost and not unsubscribed.
But generally speaking - there is no need to run this useEffect with listenUsersCollection outside of the hook. You should move it away from the screen component. Component will be cleaner and less chances to get an error. Also, easier to reuse the hook.
I prefer exporting the actual loaded user data from hooks like that, without anything else.
Example, using firebase 9 modular SDK:
import { useEffect, useMemo, useState } from "react";
import { onSnapshot, collection, query } from "firebase/firestore";
import { db } from "../firebase";
const col = collection(db, "users");
export function useUsersData(queryParams) {
const [usersData, setUsersData] = useState(undefined);
const _q = useMemo(() => {
return query(col, ...(queryParams || []));
}, [queryParams])
useEffect(() => {
const unsubscribe = onSnapshot(_q, (snapshot) => {
// Or use another mapping function, classes, anything.
const users = snapshot.docs.map(x => ({
id: x.id,
...x.data()
}))
setUsersData(users);
});
return () => unsubscribe();
}, [_q]);
return usersData;
}
Usage:
// No params passed, load all the collection
const allUsers = useUsersData();
// If you want to pass a parameter that is not
// a primitive or a string
// memoize it!!!
const usersFilter = useMemo(() => {
return [
where("firstName", "==", "test"),
limit(3)
];
}, []);
const usersFiltered = useUsersData(usersFilter);
As you can see, all the loading and cleaning-up logic is inside the hook, and the component that uses this hook is as clear as possible.

Async/await specifically with Firestore forEach (React Native with Hooks)

Can someone provide some guidance on getting a specific item within a Firestore collection before the rest of the code moves on? I want to get the user item in the users collection before iterating through the reviews collection, and this is best achieved through using async/await as far as I know, but everywhere I look on stack overflow and elsewhere, they achieve it using setups/syntax much different to what I have been using.
Here's what I've got:
import React, { useState, useEffect } from "react";
import { Image, Text, TextInput, TouchableOpacity, View } from "react-native";
import styles from "./model/AccountStyles.js";
import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view";
import { firebase } from "../firebase/config";
export default function AccountScreen({navigation }) {
const [existingReviewsArray, setExistingReviewsArray] = useState([]);
useEffect(() => {
let userID;
//get userID
firebase.auth().onAuthStateChanged((user) => {
const usersRef = firebase.firestore().collection("users");
usersRef
.doc(user.uid)
.get()
.then((document) => {
const data = document.data();
userID = data.id
});
});
//get reviews made by userID to useState
var addToReviewsArray = [];
const reviewsRef = firebase.firestore().collection('reviews');
reviewsRef.get().then((querySnapshot) => {
querySnapshot.forEach(snapshot => {
if (snapshot.data().userID == userID){
var existingReview = snapshot.data();
addToReviewsArray = ([...addToReviewsArray , existingReview]);
}
}
)
setExistingReviewsArray(addToReviewsArray);
});
}, []);
What would be the best way to go about this? Would appreciate any guidance here.
Explanation of the code below
useEffect hooks are async by design, and the Firebase API is too, so what I try to do clearly with this example is show how to implement a reactive programming approach. I don't use the async/await pattern here. This may also not be the best way of doing things - it's just the first thing that came to mind. :)
The code below "reacts" to the change of the user's auth state because initially the userId is undefined:
const [userId, setUserId] = useState(); // this means userId is undefined
The userId variable is populated in the first useEffect, which has no dependencies (the empty []), meaning that it should only run once when the component is mounted.
There is then a second useEffect hook created to listen to changes in the userId variable, which runs a function that can then use the newly populated variable.
Here's how I would do it. :)
import { useState, useEffect } from "react";
export default function AccountScreen({ navigation }) {
const [userId, setUserId] = useState();
const [existingReviewsArray, setExistingReviewsArray] = useState([]);
// Create a useEffect [with no dependencies] that runs once to get the userId
useEffect(() => {
firebase.auth().onAuthStateChanged((user) => {
const usersRef = firebase.firestore().collection("users");
usersRef
.doc(user.uid)
.get()
.then((document) => {
const data = document.data();
// Use the set state hook here to trigger the second useEffect below
setUserId(data.id);
});
});
}, []);
// Create a useEffect that is triggered whenever userId is changed
useEffect(() => {
// get reviews made by userID to useState
var addToReviewsArray = [];
const reviewsRef = firebase.firestore().collection("reviews");
reviewsRef.get().then((querySnapshot) => {
querySnapshot.forEach((snapshot) => {
if (snapshot.data().userID == userID) {
var existingReview = snapshot.data();
addToReviewsArray = [...addToReviewsArray, existingReview];
}
});
setExistingReviewsArray(addToReviewsArray);
});
}, [userId]);
// Use your `existingReviewsArray` in the render
}

React Table v7 not populating with data when using useState within the component

Edit: final solution at the bottom.
I am trying to build a simple web-app to display some data stored in firestore database, using React Table v7. I'm pretty new to React and javascript in general so forgive me if I use the wrong terminology.
At first I had put the function to fetch data inside App.jsx, passing it to state with setState within a useEffect hook, and it was working without an issue.
Then a colleague suggested that it is a good practice passing data to a component state instead of the app state, and that's where problems started to arise.
As of now, I cannot manage to populate the table. The header gets rendered but there's nothing else, and the only way I can make it show data is to make a small change in Table.jsx while npm start is running (such as adding or changing the output of any console.log) and saving the file. Only then, data is displayed.
I've been trying everything I could think of for about 2 days now (last thing I tried was wrapping Table.jsx into another component, but nothing changed).
I tried console.loging all the steps where data is involved to try and debug this, but I'm failing to understand where the problem is. Here's the output when the app loads first:
My code currently:
utility function to fetch data from Firestore
const parseData = async (db) => {
const dataArray = [];
const snapshot = db.collection('collection-name').get();
snapshot.then(
(querySnapshot) => {
querySnapshot.forEach((doc) => {
const document = { ...doc.data(), id: doc.id };
dataArray.push(document);
});
},
);
console.log('func output', dataArray);
return dataArray;
};
export default parseData;
Table.jsx
import { useTable } from 'react-table';
const Table = ({ columns, data }) => {
const tableInstance = useTable({ columns, data });
console.log('table component data received', data);
const {
getTableProps,
getTableBodyProps,
headerGroups,
rows,
prepareRow,
} = tableInstance;
return (
// html boilerplate from https://react-table.tanstack.com/docs/quick-start#applying-the-table-instance-to-markup
);
};
export default Table;
TableContainer.jsx
import { useState, useEffect, useMemo } from 'react';
import parseData from '../utils';
import Table from './Table';
const TableContainer = ({ db }) => {
const [data, setData] = useState([]);
useEffect(() => {
const getData = async () => {
const dataFromServer = await parseData(db);
setData(dataFromServer);
console.log('container-useEffect', dataFromServer);
};
getData();
}, [db]);
const columns = useMemo(() => [
{
Header: 'ID',
accessor: 'id',
},
// etc...
], []);
console.log('container data', data);
return (
<>
<Table columns={columns} data={data} />
</>
);
};
export default TableContainer;
App.jsx
import firebase from 'firebase/app';
import 'firebase/firestore';
import TableContainer from './components/TableContainer';
import Navbar from './components/Navbar';
// ############ INIT FIRESTORE DB
const firestoreCreds = {
apiKey: process.env.REACT_APP_API_KEY,
authDomain: process.env.REACT_APP_AUTH_DOMAIN,
projectId: process.env.REACT_APP_PROJECT_ID,
};
if (!firebase.apps.length) {
firebase.initializeApp(firestoreCreds);
}
const db = firebase.firestore();
function App() {
return (
<div>
<Navbar title="This is madness" />
<div>
<TableContainer db={db} />
</div>
</div>
);
}
export default App;
Edit: in the end, using the suggestions below, this is the final solution I could come up with. I was annoyed by having to wrap the API call in an async func within useEffect, so this is what I did.
utility function to fetch data from Firestore
const parseData = (db) => {
const snapshot = db.collection('collection_name').get();
return snapshot.then(
(querySnapshot) => {
const dataArray = [];
querySnapshot.forEach((doc) => {
const document = { ...doc.data(), id: doc.id };
dataArray.push(document);
});
return dataArray;
},
);
};
export default parseData;
TableContainer.jsx
Here I also added the flag didCancel within useEffect to avoid race conditions, according to this and this it seems to be a best practice.
// imports
const TableContainer = ({ db }) => {
const [data, setData] = useState([]);
useEffect(() => {
let didCancel = false;
parseData(db)
.then((dataFromServer) => (!didCancel && setData(dataFromServer)));
return () => { didCancel = true; }
}, [db]);
// ...
In parseData function this line return dataArray; is execute before the snapshot is resolved. You need to change parseData and return a Promise and resolve when data is ready:
const parseData = async (db) => {
const dataArray = [];
const snapshot = db.collection('collection-name').get();
return new Promise(resolve => {
snapshot.then(
(querySnapshot) => {
querySnapshot.forEach((doc) => {
const document = { ...doc.data(), id: doc.id };
dataArray.push(document);
});
resolve(dataArray); //--> resolve when data is ready
},
);
})
};
In your TableContainer, you initialize data with an empty array. That's being sent along to Table until you finish getting data from your server. If you don't want that to happen, you should change your default (in useState) to something like false and explicitly handle that case (e.g. display "Please wait. Loading" if data === false).
The asynchronicity in the parseData function is plain wrong (and the implementation a smidge too complex...). Reimplement it something like this...
const parseData = async (db) => {
// Note `await` here.
const snapshot = await db.collection('collection-name').get();
return snapshot.map((doc) => ({ ...doc.data(), id: doc.id }));
};
export default parseData;

React JS: Get Context Data After getting success on API Call

I am stuck at getting context data.
I have a context and a component which uses its data.
I need to get the updated data of context's variable on API call success in my component.
so How can I do that ?
Here what I have tried.
context.js
import React, { useState, createContext,useEffect } from 'react';
import {getData} from './actionMethods';
const NewContext = createContext();
function newContextProvider(props) {
const [dataValue, setData] = useState([])
useEffect(() => {
const fetchMyData = async () => {
const dataValue = await getData(); // this is an API call
setData(dataValue)
};
fetchMyData();
}, []);
return (
<NewContext.Provider
value={{
state: {
dataValue
},
actions: {
}
}}
>
{props.children}
</NewContext.Provider>
);
}
const newContextConsumer = newContext.Consumer;
export { newContextProvider, newContextConsumer, newGridContext };
myComponent.js
import React, { useState, useContext } from 'react'
import context from './context'
import deleteAPI from './actionMethods'
function myComponent(props) {
const id= 10
const {state,actions} = useContext(context)
deleteAPI(id).then(res => {
if (res){
// what should I write here to get the updated Data from the context which will call an API to get the updated data.
}
})
}
Any help would be great.
Thank You.
As a generic example, one option is to fetch the data from the server when the app loads in the front-end. From there you can send requests to modify the server data and at the same time update your local version. Something like:
Fetch data and save it to the local store: [{id: 0, name: 'first'},{id: 1, name: 'second'}]
Modify the data sending a request to the server. For example deleting an item. id: 0
Once the server responds confirming the operation was successful you can modify that data in the local store. [{id: 1, name: 'second'}]
You can handle the data using a Redux store or a React Context. For example, using a Context:
export const ItemsContext = createContext([]);
export const ItemsContextProvider = props => {
const [items, setItems] = useState([]);
const deleteItem = id => {
deleteItemsAxios(id).then(() => {
setItems(items => items.filter(item => item.id !== id));
});
};
useEffect(() => {
const fetchItems = async () => {
const items_fetched = await fetchItemsAxios();
if (items_fetched) {
setItems(items_fetched);
} else {
// Something went wrong
}
};
fetchItems();
}, []);
return (
<ItemsContext.Provider
value={{
items,
deleteItem
}}
>
{props.children}
</ItemsContext.Provider>
);
};
We define a Component that will manage the data fetch. The data items are inside a state. When the Component mounts we fetch the items and save them in the state. If we want to delete an item we first call the corresponding fetch function. Once it finishes, if it was successful, we update the state and remove that item. We use React Context to pass the items data, as well as the deleteItem function, to any component that needs them.
Let me know if you need more explanation.

Resources