How to clean up promise data in react hook? - reactjs

// Home.js
import React, { useState, useEffect } from "react";
import Todo from "../components/Todo";
import { firestore } from "../database/firebase";
export default function Home() {
const [todos, setTodos] = useState([]);
useEffect(() => {
firestore
.collection("todos")
.get()
.then(snapshot => {
setTodos(
snapshot.docs.map(doc => {
return { id: doc.id, ...doc.data() };
})
);
});
}, []);
return (
<>
{todos.map(todo => (
<Todo
key={todo.id}
id={todo.id}
title={todo.title}
></Todo>
))}
</>
);
}
I had this simple todo app, where I update todos state when I get data back from firestore.
Above useEffect set my todos as [{id:"123", title : "work"}, ...].
But, I want to put all firestore getter in one file and simple call
useEffect(() => {
getTodos().then(data=>setTodos(data))
})
Then how should I define getTodos function? I tried below code and with many variations, like adding async and await, but none of them worked.
// firestore.js
export const getTodos = () => {
return firestore
.collection("todos")
.get()
.then(snapshot => {
snapshot.docs.map(doc => {
return { id: doc.id, ...doc.data() };
});
});
};

Utilizing async/await syntax will allow you to clean things up and give you the desired result. You'll need to change things up a bit. Try something like this:
export const getTodos = async function() {
const data = await firestore.collection("todos").get();
const dataArr = data.docs.map(doc => {
return { id: doc.id, ...doc.data() };
});
return dataArr;
};

Another solution without async/await.
// firestore.js
export const getTodos = () => (
firestore
.collection("todos")
.get()
.then((snapshot) => (
snapshot.docs.map((({ id, data }) => (
{ id, ...data() }
))
))
);

Related

React & Firebase: How to update the count of a specific document in collection in real time?

Issue: How do I go about incrementing a user's number of points in their own document on each click and the result presented live in real time?
I understand real time uses a snapshot listener, but I can't seem to get it working quickly.
const handleClick = (e) => {
e.preventDefault()
const userId = e.target.id
db.collection('users').doc(userId)
.update({
housePoints: db.FieldValue.increment(1)
});
Below is the full code of the component to give an overall view of the issue I am having.
import './ClassStats.scss';
import React, { useState, useEffect } from 'react';
import Sidebar from '../../components/sidebar/Sidebar';
import Navbar from '../../components/navbar/Navbar';
import StudentView from '../../components/StudentView/StudentView';
import {
collection,
doc,
onSnapshot,
updateDoc,
serverTimestamp,
setDoc,
} from 'firebase/firestore';
import { db } from '../../firebase';
const ClassStats = () => {
const [data, setData] = useState([]);
useEffect(() => {
//LISTEN IN REAL TIME
const unsub = onSnapshot(
collection(db, 'users'),
(snapshot) => {
let list = [];
snapshot.docs.forEach((doc) => {
list.push({ id: doc.id, ...doc.data() });
});
console.log(list);
setData(list);
},
(err) => {
console.log(err);
}
);
return () => {
unsub();
};
}, []);
const handleClick = (e) => {
e.preventDefault()
const userId = e.target.id
db.collection('users').doc(userId)
.update({
housePoints: db.FieldValue.increment(1)
});
}
return (
<div className="classStats">
<Navbar />
<div className="classStatsContainer">
<Sidebar />
<div className="test">
<ul className="students">
{data.map((item) => (
<StudentView key={item.id} {...item} onClick={handleClick} />
))}
</ul>
</div>
</div>
</div>
);
};
export default ClassStats;
Thank you
Problem solved. I hope this is the best and most efficient method.
const handleClick = async (e) => {
e.preventDefault()
const userId = e.target.id
const docRef = doc(db,'users', userId);
await updateDoc(docRef, {
housePoints: increment(1)
});
}

Hi, i'm retrieving data from firestore, and checking whether to direct the user to index page or to enter details for a new user But not able to do so

React code
import React, { useEffect, useState } from "react";
import { getDocs, collection } from "firebase/firestore";
import { auth, db } from "../firebase-config";
import { useNavigate } from "react-router-dom";
function Load() {
const navigate = useNavigate();
const [accountList, setAccountList] = useState([]);
const [hasEmail, setHasEmail] = useState(false);
const accountRef = collection(db, "accounts");
Am i using useEffect correctly?
useEffect(() => {
const getAccounts = async () => {
const data = await getDocs(accountRef);
setAccountList(
data.docs.map((doc) => ({
...doc.data(),
id: doc.id,
}))
);
};
getAccounts();
emailCheck();
direct();
}, []);
checking whether email exists
const emailCheck = () => {
if (accountList.filter((e) => e.email === auth.currentUser.email)) {
setHasEmail(true);
} else {
setHasEmail(false);
}
};
Redirecting based on current user
const direct = () => {
if (hasEmail) {
navigate("/index");
} else {
navigate("/enterdetails");
}
};
return <div></div>;
}
The code compiles but doesn't redirect properly to any of the pages.
What changes should I make?
First question posted excuse me if format is wrong.
There are two problems here:
useEffect(() => {
const getAccounts = async () => {
const data = await getDocs(accountRef);
setAccountList(
data.docs.map((doc) => ({
...doc.data(),
id: doc.id,
}))
);
};
getAccounts();
emailCheck();
direct();
}, []);
In order:
Since getAccounts is asynchronous, you need to use await when calling it.
But even then, setting state is an asynchronous operation too, so the account list won't be updated immediately after getAccounts completes - even when you use await when calling it.
If you don't use the accountList for rendering UI, you should probably get rid of it as a useState hook altogether, and just use regular JavaScript variables to pass the value around.
But even if you use it in the UI, you'll need to use different logic to check its results. For example, you could run the extra checks inside the getAccounts function and have them use the same results as a regular variable:
useEffect(() => {
const getAccounts = async () => {
const data = await getDocs(accountRef);
const result = data.docs.map((doc) => ({
...doc.data(),
id: doc.id,
}));
setAccountList(result);
emailCheck(result);
direct();
};
getAccounts();
}, []);
const emailCheck = (accounts) => {
setHasEmail(accounts.some((e) => e.email === auth.currentUser.email));
};
Alternatively, you can use a second effect that depends on the accountList state variable to perform the check and redirect:
useEffect(() => {
const getAccounts = async () => {
const data = await getDocs(accountRef);
setAccountList(
data.docs.map((doc) => ({
...doc.data(),
id: doc.id,
}))
);
};
getAccounts();
});
useEffect(() => {
emailCheck();
direct();
}, [accountList]);
Now the second effect will be triggered each time the accountList is updated in the state.

Cannot use fetched data from Firestore 9, inside useEffect using Context and useReducer

I'm trying to build a react app using Firestore 9 and Context API with useReducer.
I'm stuck in a point where I'm trying to fetch data from Firestore inside the Context.js, and even though I know that the data is there and getting stored inside the state when I try to access it from another component, I'm getting an empty array.
Here's the useEffect inside Context.js
useEffect(() => {
const categoryList = [];
const fetchTasks = async () => {
console.log("Fetching taks..");
try {
const categories = [];
categoryList.forEach(async (category) => {
const ref = collection(db, "categories", category.id, "tasks");
const snapshot = await getDocs(ref);
const tasks = [];
snapshot.docs.forEach((doc) => {
tasks.push({
...doc.data(),
id: doc.id,
});
});
categories.push({ ...category, tasks: tasks });
});
dispatch({ type: "SET_CATEGORIES", payload: categories });
} catch (err) {
console.log(err);
}
};
const fetchData = async () => {
try {
const ref = collection(db, "categories");
const snapshot = await getDocs(ref);
snapshot.docs.forEach((doc) => {
categoryList.push({
...doc.data(),
id: doc.id,
});
});
} catch (err) {
console.log(err);
}
};
fetchData().then(() => fetchTasks());
}, []);
Then I'm trying to load categories inside Main.jsx component like this:
function Main() {
const { loading, categories } = useContext(TasksContext);
if (loading) {
return <h3>Loading...</h3>;
} else {
return (
<div className="flex gap-10 flex-col px-4">
{categories.map((category) => {
return <TasksList key={category.id} category={category} />;
})}
</div>
);
}
}
And here's the TasksReducer :
const tasksReducer = (state, action) => {
switch (action.type) {
case "SET_CATEGORIES": {
return {
...state,
loading: false,
categories: action.payload,
};
default:
return state;
}
};
I don't know what is the best practice to fetch a collections with their sub-collections and then merge them together.
I know it's easier to fetch data inside individual components, but I need the data in multiple places.

How do I get all documents in a Cloud Firestore collection using Version 9 of the Modular Web SDK?

I am trying to get all documents in a collection using version 9 of the Web SDK. But I am getting this error:
"TypeError: querySnapshot.map is not a function"
This is the component where I get the error:
import { useEffect, useState } from "react";
import { collection, getDocs } from "firebase/firestore";
import { db } from "../../firebase";
function CurrentUser() {
const [names, setNames] = useState([]);
async function getMakers() {
const querySnapshot = await getDocs(collection(db, "users"));
querySnapshot.map((doc) => {
setNames((doc.id = doc.data()));
});
}
getMakers();
return <div>{names.map((doc) => doc.firstName)}</div>;
}
export default CurrentUser;
querySnapshot is an instance of a QuerySnapshot object, not a JavaScript Array. This means it doesn't have the normal Array methods like map(), some() and includes(). However, it does have its own version of a forEach() method that can be used to iterate over the entries in the snapshot. If you need to access methods of a normal array, you can use the snapshot's docs property instead (which internally calls forEach() to assemble an array).
To correctly fetch the documents in the array, optionally plucking the first names of each returned document, you can use any of the following strategies:
Option 1: useState and useEffect for each user's complete data
import { useEffect, useState } from "react";
// ...
const [userDataArray, setUserDataArray] = useState([]);
useEffect(() => {
let unsubscribed = false;
getDocs(collection(db, "users"))
.then((querySnapshot) => {
if (unsubscribed) return; // unsubscribed? do nothing.
const newUserDataArray = querySnapshot.docs
.map((doc) => ({ ...doc.data(), id: doc.id }));
setUserDataArray(newUserDataArray);
})
.catch((err) => {
if (unsubscribed) return; // unsubscribed? do nothing.
// TODO: Handle errors
console.error("Failed to retrieve data", err);
});
return () => unsubscribed = true;
}, []);
return (
<div>
{ userDataArray.map((userData) => userData.firstName) }
</div>
);
Option 2: useState and useEffect for just each user's firstName
import { useEffect, useState } from "react";
// ...
const [firstNamesArray, setFirstNamesArray] = useState([]);
useEffect(() => {
let unsubscribed = false;
getDocs(collection(db, "users"))
.then((querySnapshot) => {
if (unsubscribed) return; // unsubscribed? do nothing.
const newFirstNamesArray = querySnapshot.docs
.map((doc) => doc.get("firstName"));
setFirstNamesArray(newFirstNamesArray);
// need to remove duplicates? use this instead:
// const firstNamesSet = new Set();
// querySnapshot
// .forEach((doc) => firstNamesSet.add(doc.get("firstName")));
//
// setFirstNamesArray([...firstNamesSet]);
})
.catch((err) => {
if (unsubscribed) return; // unsubscribed? do nothing.
// TODO: Handle errors
console.error("Failed to retrieve data", err);
});
return () => unsubscribed = true;
}, []);
return (
<div>
{ firstNamesArray }
</div>
);
Option 3: Make use of a tree-shakeable utility library like react-use to handle intermediate states.
import { useAsync } from 'react-use';
// ...
const remoteUserData = useAsync(
() => getDocs(collection(db, "users"))
);
if (remoteUserData.loading)
return (<div>Loading...</div>);
if (remoteUserData.error) {
console.error("Failed to load data! ", remoteUserData.error);
return (<div class="error">Failed to load data!</div>);
}
const userDataArray = remoteUserData.value;
return (
<div>
{ userDataArray.map((userData) => userData.firstName) }
</div>
);
useEffect(() =>
onSnapshot(collection(db, 'posts'),
snapshot => {
setPosts(
snapshot.docs.map(doc => ({
id: doc.id,
...doc.data()
}))
)
})
, [])

React Redux thunk - render app after dispatches finishes

My app uses React, Redux and Thunk.
Before my app renders I wish to dispatch some data to the store.
How can I make sure the ReactDOM.render() is run after all dispatches has finished?
See my code below
index.js
const setInitialStore = () => {
return dispatch => Promise.all([
dispatch(startSubscribeUser()),
dispatch(startSubscribeNotes()),
]).then(() => {
console.log('initialLoad DONE')
return Promise.resolve(true)
})
}
store.dispatch(setInitialStore()).then(()=>{
console.log('Render App')
ReactDOM.render(jsx, document.getElementById('app'))
})
Actions
export const setUser = (user) => ({
type: SET_USER,
user
})
export const startSubscribeUser = () => {
return (dispatch, getState) => {
const uid = getState().auth.id
database.ref(`users/${uid}`)
.on('value', (snapshot) => {
const data = snapshot.val()
const user = {
...data
}
console.log('user.on()')
dispatch(setUser(user))
})
}
}
export const setNote = (note) => ({
type: SET_NOTE,
note
})
export const startSubscribeNotes = () => {
return (dispatch, getState) => {
database.ref('notes')
.on('value', (snapshot) => {
const data = snapshot.val()
const note = {
...data
}
console.log('note.on()')
dispatch(setNote(note))
})
}
}
My log shows
"initialLoad DONE"
"Render App"
...
"user.on()"
"note.on()"
What I expect is for user.on() and note.on() to be logged before initialLoad DONE and Render App
Many thanks! /K
I'm pretty sure this is because startSubscribeUser and startSubscribeNotes don't return a function returning a promise.
Then, what happens in this case, is that the database.ref is not waited to be completed before executing what's in the next then.
I don't know exactly what that database variable is, but this should work :
return new Promise(resolve => {
database.ref(`users/${uid}`)
.on('value', (snapshot) => {
const data = snapshot.val()
const user = {
...data
}
console.log('user.on()')
dispatch(setUser(user))
resolve()
})
})

Resources