How to use setTimeout() along with React Hooks useEffect and setState? - javascript

I want to wait for 10 seconds for my API call to fetch the category list array from backend and store in hook state. If nothing is fetched in 10 sec, I want to set error hook state to true.
But the problem is even after the array is fetched initially, the error state is set to true and categoriesList array in state blanks out after 10 sec.
import React, { useState, useEffect } from "react";
import { doGetAllCategories } from "../helper/adminapicall.js";
const ViewCategories = () => {
let [values, setValues] = useState({
categoriesList: "",
error: false,
});
let { categoriesList, error } = values;
const preloadCategories = () => {
doGetAllCategories()
.then((data) => {
if (data.error) {
return console.log("from preload call data - ", data.error);
}
setValues({ ...values, categoriesList: data.categories });
})
.catch((err) => {
console.log("from preload - ", err);
});
};
useEffect(() => {
preloadCategories();
let timerFunc = setTimeout(() => {
if (!categoriesList && !error) {
setValues({
...values,
error: "Error fetching category list... try after some time !",
});
}
}, 10000);
return () => {
clearTimeout(timerFunc);
};
}, []);
//...further code

The problem is that the useEffect callback is a closure over categoriesList, so you'll always see the initial categories list inside the callback, and you won't see any changes to it. Now one could add categoriesList as a dependency to the useEffect hook, that way the hook will be recreated on every categoriesList change, and thus you can see the changed version:
useEffect(/*...*/, [categoriesList]);
Now the good thing is, that by updating the hook the timeout also gets canceled, so if the category list is set, we just don't have to create a new timeout:
useEffect(() => {
if(!categoriesList && !error) {
let timerFunc = setTimeout(() => {
setValues({
...values,
error: "Error fetching category list... try after some time !",
});
}, 10000);
return () => clearTimeout(timerFunc);
}
}, [!categoriesList, !error]); // some minor optimization, changes to the list don't bother this hook
I recommend you to read this blog post on useEffect by Dan Abramov.

The problem with your code is that you expect to have a change of the state of the component inside the useEffect hook. Instead, you create two variables inside the useEffect that track whether the limit of 10 sec has passed or the data is fetched. In contrary to state variables, you can expect these variables to change because they lie within the same useEffect.
export default function App() {
const [data, setData] = React.useState(null);
const [error, setError] = React.useState(null);
React.useEffect(() => {
let didCancel = false;
let finished = false;
async function fetchData() {
const data = await subscribeAPI();
if (!didCancel) {
finished = true;
setData(data);
}
}
const id = setTimeout(() => {
didCancel = true;
if (!finished) {
setError("Errorrrr");
}
}, 10000);
fetchData();
return () => {
clearTimeout(id);
};
}, []);

Related

Async/Await in useEffect(): how to get useState() value?

I have the following code snippet. Why is my limit always 0 in my fetchData? If I were to console.log(limit) outside of this function it has the correct number. Also If I dont use useState but a variable instead let limit = 0; then it works as expected
I also added limit as a dependency in useEffect but it just keeps triggering the function
const [currentData, setData] = useState([]);
const [limit, setLimit] = useState(0);
const fetchData = async () => {
console.log(limit);
const { data } = await axios.post(endpoint, {
limit: limit,
});
setData((state) => [...state, ...data]);
setLimit((limit) => limit + 50);
};
useEffect(() => {
fetchData();
window.addEventListener(`scroll`, (e) => {
if (bottomOfPage) {
fetchData();
}
});
}, []);
When you pass an empty dependency array [] to useEffect, the effect runs only once on the initial render:
If you pass an empty array ([]), the props and state inside the effect
will always have their initial values.
If you want to run an effect and clean it up only once (on mount and
unmount), you can pass an empty array ([]) as a second argument. This
tells React that your effect doesn’t depend on any values from props
or state, so it never needs to re-run. This isn’t handled as a special
case — it follows directly from how the dependencies array always
works.
useEffect docs
The initial state of limit is 0 as defined in your useState call. Adding limit as a dependency will cause the effect to run every time limit changes.
One way to get around your issue is to wrap the fetchData method in a useCallback while passing the limit variable to the dependency array.
You can then pass the function to the dependency array of useEffect and also return a function from inside of useEffect that removes event listeners with outdated references.
You should also add a loading variable so that the fetchData function doesn't get called multiple times while the user is scrolling to the bottom:
const [currentData, setData] = useState([]);
const [limit, setLimit] = useState(0);
const [loading, setLoading] = useState(false);
const fetchData = useCallback(async () => {
console.log(limit);
// Prevent multiple endpoint calls when scrolling near the end with a loading state
if (loading) {
return;
}
setLoading(true);
const { data } = await axios.post(endpoint, { limit });
setData((state) => [...state, ...data]);
setLimit((limit) => limit + 50);
setLoading(false);
}, [limit, loading]);
// Make the initial request on the first render only
useEffect(() => {
fetchData();
}, []);
// Whenever the fetchData function changes update the event listener
useEffect(() => {
const onScroll = (e) => {
if (bottomOfPage) {
fetchData();
}
};
window.addEventListener(`scroll`, onScroll);
// On unmount ask it to remove the listener
return () => window.removeEventListener("scroll", onScroll);
}, [fetchData]);

React: ES2020 dynamic import in useEffect blocks rendering

From what I understand, async code inside useEffect runs without blocking the rendering process. So if I have a component like this:
const App = () => {
useEffect(() => {
const log = () => console.log("window loaded");
window.addEventListener("load", log);
return () => {
window.removeEventListener("load", log);
};
}, []);
useEffect(() => {
const getData = async () => {
console.log("begin");
const response = await fetch(
"https://jsonplaceholder.typicode.com/todos/1"
);
const data = await response.json();
console.log("end");
};
getData();
}, []);
return null;
};
The console output is (in order):
begin
window loaded
end
However if I use ES2020 dynamic import inside the useEffect:
const App = () => {
useEffect(() => {
const log = () => console.log("window loaded");
window.addEventListener("load", log);
return () => {
window.removeEventListener("load", log);
};
}, []);
useEffect(() => {
const getData = async () => {
console.log("begin");
const data = await import("./data.json");
console.log("end");
};
getData();
}, []);
return null;
};
The output is (in order):
begin
end
window loaded
This is a problem because when data.json is very large, the browser hangs while importing the large file before React renders anything.
All the necessary and useful information is in Terry's comments. here is the implementation of what you want according to the comments:
First goal:
I would like to import the data after window has loaded for SEO reasons.
Second goal:
In my case the file I'm trying to dynamically import is actually a function that requires a large dataset. And I want to run this function whenever some state has changed so that's why I put it in a useEffect hook.
Let's do it
You can create a function to get the data as you created it as getData with useCallback hook to use it anywhere without issue.
import React, {useEffect, useState, useCallback} from 'react';
function App() {
const [data, setData] = useState({});
const [counter, setCounter] = useState(0);
const getData = useCallback(async () => {
try {
const result = await import('./data.json');
setData(result);
} catch (error) {
// do a proper action for failure case
console.log(error);
}
}, []);
useEffect(() => {
window.addEventListener('load', () => {
getData().then(() => console.log('data loaded successfully'));
});
return () => {
window.removeEventListener('load', () => {
console.log('page unmounted');
});
};
}, [getData]);
useEffect(() => {
if (counter) {
getData().then(() => console.log('re load data after the counter state get change'));
}
}, [getData, counter]);
return (
<div>
<button onClick={() => setCounter((prevState) => prevState + 1)}>Increase Counter</button>
</div>
);
}
export default App;
Explanation:
With component did mount, the event listener will load the data.json on 'load' event. So far, the first goal has been met.
I use a sample and simple counter to demonstrate change in the state and reload data.json scenario. Now, with every change on counter value, the getData function will call because the second useEffect has the counter in its array of dependencies. Now the second goal has been met.
Note: The if block in the second useEffect prevent the getData calling after the component did mount and first invoking of useEffect
Note: Don't forget to use the catch block for failure cases.
Note: I set the result of getData to the data state, you might do a different approach for this result, but the other logics are the same.

React Hook useEffect has a missing dependency: 'getContacts'

Before posting the below, I have reviewed similar posts on stackoverflow but none resolved my issue.
I'm new to react and fetching data from firestore database. The below code works as required but getting this prompt within react
import React, {useState, useEffect} from 'react'
import {db} from '../firebase'
const ListRecord = () => {
const [details, setDetails] = useState([]);
useEffect(() => {
getContacts()
},[]);
const getContacts = async() => {
await db.collection('contacts').get().then((querySnapshot) => {
let arr = []
querySnapshot.forEach((doc) => {
arr.push({id: doc.id, value: doc.data()})
});
setDetails(arr);
});
console.log(details);
return details
}
return (
<div>
<h2>List Contact Details</h2>
</div>
)
}
export default ListRecord
As per other similar posts I tried moving the getContacts function inside useEffect body which make the prompt disapper but the getContacts function goes in a continuous loop.
I'm not sure what I'm missing here and any help would be appreciated.
There are different potential solutions:
1. Move getContacts() inside the useEffect() hook:
If you call getContacts() only once and only when the component mounts for the first time, this is probably the most logic solution.
useEffect(() => {
const getContacts = async () => {
await db.collection('contacts').get().then((querySnapshot) => {
let arr = []
querySnapshot.forEach((doc) => {
arr.push({
id: doc.id,
value: doc.data()
})
});
setDetails(arr);
});
//console.log(details);
//return details // why are you returning details?
}
getContacts()
}, [setDetails]); // setDetails() is granted to never change therefore the hook will never re-run
or, of course, you can use an IIFE:
useEffect(() => {
(async function() {
// ... same body as getContacts
})()
}, [setDetails])
2. Use a useCallback() hook:
This is something you might want to do if getContacts() is called more than once (for example, when the component mounts and every time some prop changes or when you click on some button)
const getContacts = useCallback(async () => {
await db.collection('contacts').get().then((querySnapshot) => {
let arr = []
querySnapshot.forEach((doc) => {
arr.push({
id: doc.id,
value: doc.data()
})
});
setDetails(arr);
});
//console.log(details);
//return details // why are you returning details?
}, [setDetail]); // setDetails() is granted to never change therefore getContacts() will never be re-created
useEffect(() => {
getContacts()
}, [getContacts]); // as getContacts() never changes, this will run only once
3. Move getContacts() out of the component and make it an independent function:
This can make sense if you want to reuse the same logic into other components:
// getContacts.js file
// OR this code could be in the ListRecord.js file but **outside** the component,
// although, in this case, solutions (1) or (2) would make more sense
import { db } from 'path/to/firebase'
export async function getContacts() {
await db.collection('contacts').get().then((querySnapshot) => {
let arr = []
querySnapshot.forEach((doc) => {
arr.push({
id: doc.id,
value: doc.data()
})
});
return arr; // this time you HAVE TO return arr
});
}
// ListRecord.js file
import React, { useState, useEffect } from 'react';
import { getContacts } from 'path/to/getContacts.js';
const ListRecord = () => {
const [details, setDetails] = useState([]);
useEffect(async () => {
const arr = await getContacts();
if (arr && arr.length > 0) setDetails(arr);
}, [setDetails]);
//...
}
I suggest you have a look at how useEffect and its dependency list works in the official document.
In short, do the following:
useEffect(() => {
getContacts()
}, [getContacts]);
This means when getContacts changes, the useEffect will be re-run.

React Custom Hook and Firestore Snapshot

I created a React custom hook which listens to firestore database, i followed the documentation in firebase website but I'm having trouble when I re-render the component.
When I refresh my app the useEffect hook runs correctly but the listener inside unsubscribe() method doesn't. Here is the full code of the hook.
It works fine when the code recompiles itself after I save something in a component (first-render), but when it re-renders it shows as it if was always loading but it never executes the listener. Hope you can help me!
import { db } from "initFirebase";
import { useEffect, useState } from "react";
interface Payment {
name: string;
email: string;
phone: string;
course: string;
pagado: boolean;
}
export const usePaymentsCollection = () => {
const [payments, setPayments] = useState<Payment[]>([]);
const [isLoading, setIsLoading] = useState<boolean>(true);
const [error, setError] = useState<any>(null);
useEffect(() => {
const unsubscribe = () => {
try {
db.collection("payments").onSnapshot(
(snapshot) => {
if (snapshot.docs.length) {
console.log("simona");
let allDocs: Payment[] = [];
snapshot.docs.map((doc) => {
console.log(doc.data());
return allDocs.push(doc.data() as Payment);
});
setPayments(allDocs);
setIsLoading(false);
} else {
console.log("isEmpty");
setIsLoading(false);
}
},
(error) => {
// ...
}
);
} catch (e) {
setError(e);
}
};
return () => unsubscribe();
}, [db]);
return {
payments,
isLoading,
error,
};
};
The unsubscribe anonymous function you have provided is not a snapshot listener unsubscribe method. It is a function which creates a snapshot listener (and ignores the unsubscribe method returned from db.collection("payments").onSnapshot()).
You should set unsubscribe to the value returned from db.collection("payments").onSnapshot():
useEffect(() => {
const unsubscribe = db.collection("payments").onSnapshot(
(snapshot) => {
// ...
},
(error) => {
// ...
}
);
return unsubscribe;
}, [db]);
Additionally, by passing the array [db] into the useEffect hook, it will run again on rerender when the db state/prop is updated. Be sure that this is what you want - it probably isn't, since db is likely not a state/prop.

Firebase with useeffect cleanup function

I'm trying to get all data with the 'public' parameter of a firebase collection and then use useffect, but the console accuses error.
I took this structure from firebase's documentation:
https://firebase.google.com/docs/firestore/query-data/get-data#get_multiple_documents_from_a_collection
but the console says 'Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function'
So I went to this other page:
https://firebase.google.com/docs/firestore/query-data/listen#detach_a_listener
But I'm not using onSnapshot, besides firebase documentation seems wrong to me, since unsub is not a function.
useEffect(() => {
let list = []
const db = firebase.firestore().collection('events')
let info = db.where('public', '==', 'public').get().then(snapshot => {
if (snapshot.empty) {
console.log('No matching documents.');
setLoading(false)
return;
}
snapshot.forEach(doc => {
console.log(doc.id, "=>", doc.data())
list.push({
id: doc.id,
...doc.data()
})
})
setEvents(list)
setLoading(false)
})
.catch(error => {
setLoading(false)
console.log('error => ', error)
})
return () => info
}, [])
You need to invoke the listener function to detach and unmount.
https://firebase.google.com/docs/firestore/query-data/listen#detach_a_listener
useEffect(() => {
let list = []
const db = firebase.firestore().collection('events')
let info = ...
return () => info() # invoke to detach listener and unmount with hook
}, [])
You don't need to clear a .get(), it's like a normal Fetch api call.
You need to make sure your component is still mounted when you setLoading
Example:
// import useRef
import { useRef } from 'react'
// At start of your component
const MyComponent = () =>{
const isMounted = useRef(false)
useEffect(() => {
isMounted.current = true;
return () => isMounted.current = false;
}, [])
// Your query
db.where('public', '==', 'public').get().then(snapshot => {
if(isMounted.current){
// set your state here.
}
})
return <div></div>
}

Categories

Resources