How to create a useEffect that only updates when firestore updates? [duplicate] - reactjs

This question already has answers here:
ReactJS and Firebase Quota Reached Very Fast with Small Data
(1 answer)
VERY High number of reads in Firestore database in my React Project
(1 answer)
Firebase Reads Suddenly Spiked with 54k [duplicate]
(1 answer)
Closed last month.
First off, let me say that I probably worded my question terribly... sorry.
I currently have a useEffect in my application that when you load my page it takes the data from my Firestore collection and sets it to an array to map on screen with a component. It works perfectly fine, however after about 10 minutes of running my application I receive the error "#firebase/firestore: Firestore (9.15.0): Uncaught Error in snapshot listener: FirebaseError: [code=resource-exhausted]: Quota exceeded.".
I added a console log and it looks like this is because my useEffect is constantly trying to read the data from the collection in firestore.
My question is, is there a way to only make this useEffect update the data / run when a new collection is added or deleted / modified?
Code:
import React, { useState, useEffect, useRef } from 'react';
import '../index.css';
import './Home.css';
import Note from '../components/Note';
import { useAuth } from '../contexts/AuthContext';
import { db } from '../firebase';
import { ReactComponent as Add } from '../imgs/add.svg';
import { doc, onSnapshot, query, collection } from 'firebase/firestore';
function Home() {
// Firebase states
const { currentUser } = useAuth();
const noteboardCollectionRef = collection(db, `users/${currentUser.uid}/noteboard-app`);
// useStates
const [notes, setNotes] = useState([]);
//useEffect
useEffect(()=>{
const q = query(noteboardCollectionRef)
const noteboardFirebase = onSnapshot(q, (querySnapshot)=>{
let noteArr = []
querySnapshot.forEach((doc)=>{
noteArr.push({...doc.data(), id: doc.id})
});
setNotes(noteArr);
console.log(notes)
})
return noteboardFirebase;
})
// Start of all functions
return (
<>
<div className='home-container'>
<div className='home-header flex'>
<h1 className='font-carter-one'>Noteboard</h1>
<div className='home-header-dark-container'>
<label className='font-carter-one'>Dark Mode</label>
<span className='home-header-dark-mode'>
<input type='checkbox' checked/>
<span className='dark-mode-slider pointer'/>
</span>
</div>
</div>
<div className='home-body flex-center-all'>
<div className='home-new-note flex-center-all flex-column pointer' onClick={()=>{setAddNoteModal(true)}}>
<Add className='pointer' id='new-note'/>
<h2 className='font-carter-one'>Add Note</h2>
</div>
{notes.map(((note, index) => <Note key={index} note={note} />))}
</div>
</div>
</>
)
}
export default Home;
Thank you in advanced!

You are facing this error because you dont have dependencies in the useEffect i.e
useEffect(()=>{...
},[]) // You are missing this []
Because of which the useEffect runs every time the page is rendered , which is causing to make unlimited requests to the server which is leading to quota exceeded error in firebase
If array is null i.e [] then it runs only the first time the page is rendered.
If you want the useEffect to run only when the firestore is changed add dependency of notes i.e [notes].
Now useEffect will run only when there is change in notes !!
Your final code should look like:
useEffect(()=>{
const q = query(noteboardCollectionRef)
const noteboardFirebase = onSnapshot(q, (querySnapshot)=>{
let noteArr = []
querySnapshot.forEach((doc)=>{
noteArr.push({...doc.data(), id: doc.id})
});
setNotes(noteArr);
console.log(notes)
})
return noteboardFirebase;
}[notes]) // <-- add dependency of notes

Add dependency to [] like this:
useEffect(()=>{
doSomething()
},[dependency])
when dependency is changed, doSomething() will run again.
You can see detail in here!
So, as for your question you should edit code to this:
//useEffect
useEffect(()=>{
const q = query(noteboardCollectionRef)
const noteboardFirebase = onSnapshot(q, (querySnapshot)=>{
let noteArr = []
querySnapshot.forEach((doc)=>{
noteArr.push({...doc.data(), id: doc.id})
});
setNotes(noteArr);
console.log(notes)
})
return noteboardFirebase;
},[notes])

You could use onSnapshot() to make this happen something like this ;
import { useEffect, useState } from 'react';
import { firestore } from './firebase';
function MyComponent() {
const [document, setDocument] = useState(null);
useEffect(() => {
const unsubscribe = firestore
.doc('my-collection/my-document')
.onSnapshot((doc) => {
setDocument(doc.data());
});
return () => {
unsubscribe();
};
}, []);
return (
<div>
{document ? (
<div>
<h1>{document.title}</h1>
<p>{document.body}</p>
</div>
) : (
<p>Loading...</p>
)}
</div>
);
}

Related

loading components twice, probably because of useEffect wrong set-up

I have built a ToDo React App (https://codesandbox.io/s/distracted-easley-zjdrkv) that does the following:
User write down an item in the input bar
User hit "enter"
Item is saved into the list below (local storage, will update later)
There is some logic to parse the text and identify tags (basically if the text goes "#tom:buy milk" --> tag=tom, text=buy milk)
The problem I am facing are:
useEffect runs twice at load, and I don't understand why
After the first item gets saved, if I try saving a second item, the app crashes. Not sure why, but I feel it has to do with the point above...and maybe the event listener "onKeyDown"
App
import { useState, useEffect } from 'react'
import './assets/style.css';
import data from '../data/data.json'
import InputBar from "./components/InputBar/InputBar"
import NavBar from "./components/NavBar/NavBar"
import TabItem from "./components/Tab/TabItem"
function App() {
const [dataLoaded, setDataLoaded] = useState(
() => JSON.parse(localStorage.getItem("toDos")) || data
)
useEffect(() => {
localStorage.setItem("toDos", JSON.stringify(dataLoaded))
console.log('update')
}, [dataLoaded])
function deleteItem(id){
console.log(id)
setDataLoaded(oldData=>{
return {
...oldData,
"items":oldData.items.filter(el => el.id !== id)
}
})
}
return (
<div className='container'>
<NavBar/>
<InputBar
setNewList = {setDataLoaded}
/>
{
//Items
dataLoaded.items.map(el=>{
console.log(el)
return <TabItem item={el} key={el.id} delete={deleteItem}/>
})
}
</div>
)
}
export default App
InputBar
import { useState, useEffect } from 'react'
import { nanoid } from 'nanoid'
import '../../assets/style.css';
export default function InputBar(props){
const timeElapsed = Date.now();
const today = new Date(timeElapsed);
function processInput(s) {
let m = s.match(/^(#.+?:)?(.+)/)
if (m) {
return {
tags: m[1] ? m[1].slice(1, -1).split('#') : ['default'],
text: m[2],
created: today.toDateString(),
id:nanoid()
}
}
}
function handleKeyDown(e) {
console.log(e.target.value)
console.log(document.querySelector(".main-input-div input").value)
if(e.keyCode==13){
props.setNewList(oldData =>{
return {
...oldData,
"items" : [processInput(e.target.value), ...oldData.items]
}
}
)
e.target.value=""
}
}
return(
<div className="main-input-div">
<input type="text" onKeyDown={(e) => handleKeyDown(e)}/>
</div>
)
}
Tab
import { useState } from 'react'
import "./tab-item.css"
import { FontAwesomeIcon } from '#fortawesome/react-fontawesome'
import { faTrash } from "#fortawesome/free-solid-svg-icons";
export default function TabItem(props) {
return (
<div className="tab-item">
<div className="tab-item-text">{props.item.text}</div>
<div className="tab-item-actions">
<FontAwesomeIcon icon={faTrash} onClick={()=>props.delete(props.item.id)}/>
</div>
<div className="tab-item-details">
<div className="tab-item-details-tags">
{
props.item.tags.map(el=><div className="tab-item-details-tags-tag">{el}</div>)
}
</div>
</div>
<div className="tab-item-date">{props.item.created}</div>
</div>
)
}
The above answer is almoost correct. I am adding more info to the same concepts.
useEffect running twice:
This is most common ask in recent times. It's because the effect runs twice only in development mode & this behavior is introduced in React 18.0 & above.
The objective is to let the developer see & warn of any bugs that may appear due to a lack of cleanup code when a component unmounts. React is basically trying to show you the complete component mounting-unmounting cycle. Note that this behavior is not applicable in the production environment.
Please check https://beta-reactjs-org-git-effects-fbopensource.vercel.app/learn/synchronizing-with-effects#step-3-add-cleanup-if-needed for a detailed explanation.
App crashes on second time: It's probably because you are trying to update the input value from event.target.value if you want to have control over the input value, your input should be a controlled component meaning, your react code should handle the onChange of input and store it in a state and pass that state as value to the input element & in your onKeyDown handler, reset the value state. That should fix the crash.
export default function InputBar(props){
const [inputVal, setInputVal] = useState("");
function handleKeyDown(e) {
console.log(e.target.value)
console.log(document.querySelector(".main-input-div input").value)
if(e.keyCode==13){
props.setNewList(oldData =>{
return {
...oldData,
"items" : [processInput(e.target.value), ...oldData.items]
}
}
)
setInputVal("")
}
}
return(
<div className="main-input-div">
<input
type="text"
value={inputVal}
onChange={(e) => {setInputVal(e.target.value)}}
onKeyDown={(e) => handleKeyDown(e)}
/>
</div>
)
}
Hope this helps. Cheers!
Your app is using strict mode, which in a development mode renders components twice to help detect bugs (https://reactjs.org/docs/strict-mode.html#detecting-unexpected-side-effects).
root.render(
<StrictMode>
<App />
</StrictMode>
);
As for the crash, I think it's happening due to props.setNewList being an asynchronous call and the resetting of e.target.value - something like this seemed to fix it for me:
function handleKeyDown(e) {
console.log(e.target.value)
console.log(document.querySelector(".main-input-div input").value)
if(e.keyCode==13){
const inputVal = e.target.value;
props.setNewList(oldData =>{
return {
...oldData,
"items" : [processInput(inputVal), ...oldData.items]
}
}
)
e.target.value=""
}
}
I will add, that using document.querySelector to get values isn't typical usage of react, and you might want to look into linking the input's value to a react useState hook.
https://reactjs.org/docs/forms.html#controlled-components

Why is my state value undefined when I pass it and try use it?

I've fetched movies from an API and stored the movie object in a state value called 'movies'.
I then pass this state as a prop to my slideshow component where I want to access the poster_path property of this object to display.
When I receive the object, I destructure it and take the 'poster_path' and store it as a variable called 'posters' eg: let posters = movies.poster_path.
When I console log 'movies' it gives me all of the movies, but when I log this variable it is giving me 'undefined'. I'm sure I'm missing something small but I can't figure it out. (I don't show the full route that my state travels but it starts in App.js and it's received in the Slideshow component).
Basically, how do I get this value so I can then use it within my slideshow?
Apologies in advance for my code as I'm still pretty new to React.
import React, { useState, useEffect } from 'react';
// import { BrowserRouter as Router, Routes, Route, Link } from 'react-router-dom';
//Components
import Header from './components/Header'
import MusicPlayer from './components/MusicPlayer';
//Pages
import Home from './pages/Home';
import Kids from './pages/Kids';
import Movies from './pages/Movies';
import Music from './pages/Music'
//CSS
import './css/Header.css'
import './App.css';
import './css/Movies-Kids.css'
import './css/Music.css'
import './css/Home.css'
import './css/HeroSlider.css'
import './css/FilterButton.css'
import './css/MusicPlayer.css'
import { Delete } from '#mui/icons-material';
function App() {
// * MOVIE API *
//State
const [movies, setMovies] = useState([])
//API URL
const url = 'https://api.themoviedb.org/3/discover/movie?api_key=APIKEY&with_genres=28/';
//Async function to fetch API
async function getMoviesData (url) {
await fetch(url).then(res => res.json()).then(data => setMovies(data.results))
}
//Use Effect
useEffect(() => {
getMoviesData(url);
}, [])
return (
<div className='app'>
<div className="header">
<Header Home={Home} Movies={Movies} Kids={Kids} Music={Music} movies={movies} />
</div>
<div className="music-player">
{/* <MusicPlayer /> */}
</div>
</div>
)
}
export default App
import React, { useState, useEffect } from 'react'
//Images Posters
import BackDrop from '../images/sandwich.jpg'
import FoodBanner from '../images/foodbanner.jpg'
import Batman from '../images/batman.jpg'
import Pulpfiction from '../images/pulp-fiction.jpg'
// const posters = [Pulpfiction, Batman, BackDrop, FoodBanner, FoodBanner];
const delay = 50000;
function Slideshow({movies}) {
let posters = movies.poster_path
console.log(posters)
//State
const [index, setIndex] = useState(0)
//UseEffect
useEffect(() => {
setTimeout(
() =>
setIndex((prevIndex) =>
prevIndex === posters.length - 1 ? 0 : prevIndex + 1
),
delay
);
return () => {};
}, [index]);
return (
<div className="slideshow">
<div className="slideshowSlider" style={{ transform: `translate3d(${-index * 100}%, 0, 0)` }}>
{posters.map((poster, index) => (
<img src={poster} className="slide" key={index} style={{ poster }}/>
))}
</div>
{/* <div className="slideshowDots">
{posters.map((_, index) => (
<div key={index} className="slideshowDot"></div>
))}
</div> */}
</div>
);
}
export default Slideshow
From comments on the question...
When the 'movies' state is passed to the header through to the slideshow it is not undefined. When I log 'movies' it works.
Indeed. It's an array. That array is initially empty here:
const [movies, setMovies] = useState([])
And presumably your AJAX operation sets it to a potentially non-empty array.
The problem is when I try to log a property of that movie it gives me undefined. eg: movies.poster_path returns 'undefined'
What is "that movie"? movies is an array. It's a collection of zero or more "movies". If nothing else, look at the semantics of your variable. It's called movies, not movie.
The code has no way of knowing which specific element from the collection you are referring to unless you tell it. An array has no property called poster_path, but an object in the array could:
movies[0].poster_path
(This of course assumes there is at least one object in the array. There may be none.)
In fact, you're even expecting poster_path to also be an array here:
let posters = movies.poster_path
// and later...
posters.map((poster, index) => (
So... Is your data an array or isn't it? Sometimes you think it is, sometimes you think it isn't. It has to be one or the other, it can't simultaneously be both.
If movies is indeed an array (and the code indicates that it is), and if objects within that array have a property called poster_path, and if you expect posters to be an array of that property (and the code indicates that you do), then make it an array of that property:
let posters = movies.map(m => m.poster_path)
Overall you just need to be aware of what data is in your variables. Be aware of what is a collection of objects and what is a single instance of an object.
movies is initially an array and movies.poster_path is definitely undefined. you may want to use a useEffect hook to monitor when the data is updated.
so change posters to useState and set the state when movie is updated with a useEffect.
const [posters, setPosters] = useState("")
useEffect(() => {setPosters(movies.poster_path)},[movies])
Hope this helps.

Why is my component failing to run when I call it?

I am struggling to find why my component is not responding to being called by its parent. I am trying to integrate Cloud Firestore with code that previously ran using Redux. My first goal is to populate my List with data from Firestore.
Here are my (simplified) components in question:
// List.js
import React, { useEffect, useState } from "react";
import db from "../../db";
import { onSnapshot, query, collection, orderBy } from "firebase/firestore";
import TaskItem from "./TaskItem";
const List = () => {
const [taskList, setTaskList] = useState([]); // Currently assumes DB never empty, populates on initial render
const [isInitialRender, setIsInitialRender] = useState(true);
// Firestore
const ref = collection(db, "Tasks");
const q = query(ref, orderBy("listIndex"));
useEffect(() => {
// Execute only on initial render
if (isInitialRender) {
// Populate task list
onSnapshot(q, (querySnapshot) => {
setTaskList(() => querySnapshot.docs)
}, (error) => {
console.log(error)
})
};
setIsInitialRender(() => false);
}, []);
return (
<>
<h2>List</h2>
{taskList.forEach((task) => ( // console-logging `task` here will output correct data
<ul key={task.data().key}>
<TaskItem
id={task.data().key}
// docRef={taskDoc}
/>
</ul>
))
}
</>
);
};
export default List;
// TaskItem.js
import React from "react";
const TaskItem = (props) => {
console.log('This will not print')
const submitHandler = () => console.log('Submitted');
return (
<form onSubmit={submitHandler}>
<input
autoFocus
type="text"
/>
</form>
);
};
export default TaskItem;
I have tried:
Populating the state with the data from each document (rather than assigning it directly), then passing the contents as props. This led to (I believe) an infinite loop, and ideally I would like to pass the actual DocumentReference to the TaskItem anyways. So this was a bust for me.
Returning [...querySnapshot.docs], or even (prev) => prev = [...querySnapshot.docs] in the state setter. No response from TaskItem().
Decomposing the taskList state into a new dummy array, and using that array to populate the props for TaskItem.
I know that the task data is being fetched successfully because I can satisfactorily log taskList's contents from the map function in List's return statement. But it seems like TaskItem() never runs.
Does anyone see my error here?
edit: sorry I assumed you were using map. I'm not sure why your forEach isn't working but map would work, from my example
edit 2: you probably are looking to use map because you want to transform every element in the array: JavaScript: Difference between .forEach() and .map()
you forgot to return something from the map, and maybe need {} instead.
try
{taskList.forEach((task) => {
return (
<ul key={task.data().key}>
<TaskItem
id={task.data().key}
// docRef={taskDoc}
/>
</ul>
)
})

Unable to map over result of async fetch request in React Hook [duplicate]

This question already has answers here:
Reactjs async rendering of components
(3 answers)
Closed 1 year ago.
I've found quite a few issues related to this but I don't seem to be able to find a solution that works and I'm not sure why.
I have a async fetch request in a react functional component. It returns some dummy JSON data, which I then want to map over.
Everything is returned correctly in the console.log (and in Postman), however when it comes to mapping the result I get
TypeError: Cannot read property 'map' of undefined
import "./App.css";
import React, { useState, useEffect } from "react";
function App() {
const [dummyData, setDummyData] = useState([]);
useEffect(() => {
fetch("https://jsonplaceholder.typicode.com/todos/1")
.then((res) => {
return res.json();
})
.then((data) => {
setDummyData(data);
});
}, []);
return (
<div className="App">
{console.log(dummyData)}
{dummyData.map((d) => {
return <h1> {d}</h1>;
})}
</div>
);
}
export default App;
Can anyone help me understand why? I feel there may be some fundamental gaps in my knowledge on this?? Or am I missing something simple?
Adding a codesandbox to demo and initialising the state of dummyData
https://codesandbox.io/s/objective-benz-t1zpi?file=/src/App.js
https://jsonplaceholder.typicode.com/todos/1 returns an Object, Not an Array. dummyData is replaced with Object(once you receive response) which does not have array methods. That's why you're getting error.
I think you need this https://jsonplaceholder.typicode.com/todos.
import React, { useState, useEffect } from "react";
function App() {
const [dummyData, setDummyData] = useState([]);
useEffect(() => {
fetch("https://jsonplaceholder.typicode.com/todos")
.then((res) => {
return res.json();
})
.then((data) => {
setDummyData(data);
});
}, []);
return (
<div className="App">
{dummyData.map((d) => {
return (
<h1>
{d.id} - {d.title}
</h1>
);
})}
</div>
);
}
export default App;
Demo
You have not initiaized the state - dummyData
Change the state declaration to:
const [dummyData, setDummyData] = useState([]);

Needs Help To Troubleshoot Fetching Single Document From Firebase Database As Detailed Page

I'm try to get single document as detail information from Firebase database under collection "books", however my array method map does not recognize as function due to the render produce "undefined". Somehow render again and produce the object value in log. I posted the screenshot of the log above, hoping somebody help me out, thanks!!!!!
import React, {useState, useEffect} from 'react'
import {Link} from 'react-router-dom'
import firebase from '../config/fbConfig'
const BookDetails = (props) => {
const [books, setBooks] = useState([])
useEffect(() => {
const db = firebase.firestore()
const id = props.match.params.id
var docRef = db.collection("books").doc(id);
docRef.get().then(doc => {
if(doc.exists){
const data = doc.data()
console.log("Document data:", data)
setBooks(data)
}else {
console.log("No such document!");
}
}).catch(error => {
console.log("Error getting document:", error);
})
}, [])
console.log('this log is before return', books.title)
return (
<div className="book_details">
<Link to="/"><h2>Home</h2></Link>
{console.log("this log is in the return method", books.title)}
<h1>The Summary Of the Book </h1>
{books.map( book => <ul key = "book.id" >
<li>Book Title: {book.title}</li>
<li>Book Author: {book.author}</li>
<li>Book Summery: {book.brief}</li>
</ul>)}
</div>
)
}
export default BookDetails
Because you are testing whether books is undefined and only call the map function if it is defined (i.e. {books && books.map( [...] )}), the problem must lie somewhere else.
You are fetching a single document from your Firebase database. Therefore, the returned data will not be an array but an object which does not have the map function in its prototype. You can verify this from your console logs.
Your component renders twice because you are changing its state inside the useEffect via setBooks(data).
const db = firebase.firestore()
const id = props.match.params.id
First of all move these lines inside of useEffect.
Coming to the problem
You are fetching a single doc(object) from firebase and saving it in a state which is an array. Change your useState to
const \[book, setBook\] = useState(undefined) // or useState({})
Change your return to
return (
<div className="book_details">
<Link to="/"><h2>Home</h2></Link>
{console.log("this log is in the return method", books.title)}
<h1>The Summary Of the Book </h1>
{book && <div key={book.id}> {book.brief} </div>}
</div>
)
// or {Object.keys(book).length !== 0 && <div key={book.id}> {book.brief} </div>}
if you have used empty object in useState.

Resources