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

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
}

Related

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.

setState inside useEffect cause infinite loop

I want to use the list to seData and setUserData. I am using data to map through the list of users and display on the table. I am using the userData to get user information in different components. This is causing infinite loop.
import { useState, useEffect, useContext } from 'react';
import { collection, onSnapshot } from 'firebase/firestore';
import { db, auth } from '../../firebase';
import { UserContext } from '../../Contexts/UserContext';
const Datatable = () => {
const {userData, setUserData} = useContext(UserContext);
const [data, setData] = useState([]);
const [isEditing, setIsEditing] = useState(false);
const [currentId, setCurrentId] = useState("");
useEffect(() => {
const getUserData = () => {
onSnapshot(collection(db, "users"), (snapshot) => {
let list = [];
snapshot.docs.forEach((doc) => {
list.push({ id: doc.id, ...doc.data() });
setData(list);
setUserData(list)
});
}, (err) => {
console.log(err);
});
}
getUserData();
}, [])
}
Updating values to the state should not be done in useEffect. You have to create a function outside and update the state value.
If you do so, You will be getting Warning: Maximum update depth exceeded. error.
This happens because once if you update a state the reference of the state will be changed and component rerender happens. Again since the reference is changed. The useEffect will be called again and this happens infinitely which the react stops after a certain extent.

Firebase snapshot stored in React Context

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.

Set default state after object has been loaded

I am using custom made useFirestore hook to retrieve file objects from my firestore collection.
import { useState, useEffect } from "react";
import { app } from "../firebase/firebase_storage";
const useFirestore = (collection) => {
const [songs, setSongs] = useState([]);
useEffect(() => {
const unsub = app
.firestore()
.collection(collection)
.onSnapshot((snap) => {
let songsArr = [];
snap.forEach((song) => {
songsArr.push({ ...song.data(), id: song.id });
});
setSongs(songsArr);
});
return () => unsub();
}, [collection]);
return { songs };
};
export default useFirestore;
Then I am storing this collection into a variable
const tracks = useFirestore("songs");
console.log(tracks) is giving
{songs: Array(2)}
I want to set the first track to be the default one which will be loaded into the waveform and I am using useState.
const [selectedTrack, setSelectedTrack] = useState(tracks.songs[0]);
This is not working because when the default state is set, the value I am passing is undefined because I am assuming it is not loaded. When I set some objects with placeholder data this works.
So my idea was that somehow postpone assigning default state until the object loads and it is not undefined.
Does anyone have an idea on how to achieve this?
You can use useEffect and set default state after tracks is initialized.
Try:-
const tracks = useFirestore("songs");
const [selectedTrack, setSelectedTrack] = useState('');
useEffect(() => {
setSelectedTrack(tracks.songs[0]);
}, [tracks])

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