Unnecessary parameter in useEffect dependency array - javascript

I'm creating an application where users can create and share notes.
To share each other's notes users have to send requests to specific users.
The requests are fetched whenever home is loaded.
However, requests is a context since it is also consumed in the toolbar and requests page to show the presence of the requests
When I'm using setRequsts method of the context to set all the requests after home loads, the fetch goes into an infinite loop of /noteand /me URLs, since the setRequests method is also provided in the dependency array of useEffect
When removed, useEffect show missing dependencies. What's the work around?
const {setRequests } = useContext(RequestsContext)
const [notes, setNotes] = useState([])
const [fetched, setFetched] = useState('')
const { isAuthenticated } = props
const {page}=useContext(PageContext)
const [sortBy,setSortBy]=useState('latest')
useEffect(() => {
const fetch = async () => {
try {
let url = 'http://192.168.56.1:5000/api/v1/note', p, sort
if (page) p = `?page=${page}&limit=12`
if (sortBy === 'latest') {
sort=''
} else if (sortBy === 'most_liked') {
sort='&sort=likes'
}
const res = await Axios.get(url+p+sort)
setNotes(res.data.data)
if (res.data.data.length > 0) {
setFetched('Y')
} else {
setFetched('N')
}
} catch (err) {
console.log(err)
} finally {
if (isAuthenticated) {
const fetch = async () => {
const res = await axios.get(`user/me`)
if (res.data.data.createdPosts.length > 0) {
const arr = res.data.data.createdPosts.map(el => el.request)
console.log(arr)
setRequests(arr)
}
}
fetch()
}
}
}
fetch()
}, [isAuthenticated, /* setRequests, */ page, sortBy])

The problem is that the context provides a technically different setRequests function on each render (that have a different address). This causes useEffect to fire on each render.
To work around this, you could wrap setRequests in a useCallback() hook, like so:
// ...
const wrappedSetRequests = useCallback(setRequests, []);
// ...
useEffect(() => {
// do your stuff using 'wrappedSetRequests' instead of setRequests.
}, [ wrappedSetRequests /*...*/ ]);

Related

How to handle refresh for data filtering in React useEffect?

I have the following code in my React component:
const { id } = useParams();
const { tripData, facilityData } = useContext(AppContext);
const [data, setData] = useState([]);
useEffect(() => {
const idResults = facilityData.filter(facility => facility.id === id);
if (idResults.length > 0) {
setData(idResults[0]);
}
}, [])
Where:
[data, SetData] is the state that is used to handle populating a container
facilityData is data accessed from my app context
id is accessed from the URL
What seems to happen is that the data loads the first time without fault, but it errors out when hosted on the actual site (on localhost, it waits and eventually loads). To try to get a better idea of what was happening, I tried the following code:
const { id } = useParams();
const { tripData, facilityData } = useContext(AppContext);
const [data, setData] = useState([]);
useEffect(() => {
const idResults = facilityData.filter(facility => facility.id === id);
if (idResults.length > 0) {
setData(idResults[0]);
} else if (idResults.length === 0) {
console.log(`id: ${id}`)
console.log(`len: ${idResults}`)
}, [])
On localhost, on refresh, it console logs the actual id but then console logs the empty array before finally loading the data.
What I'm wondering is why this is the observed behavior. The "id" value seems to be constantly available, but the filter doesn't seem to run prior to the site loading. Is there a way to prevent this?
EDIT:
This is how I get the data (from Firebase)
App.js
import { collection, getDocs } from "firebase/firestore";
import { db } from "./firebase";
const [truckData, setTruckData] = useState([]);
const [facilityData, setFacilityData] = useState([]);
const [tripData, setTripData] = useState([]);
useEffect(() => {
const fetchData = async (resource, setter) => {
let list = [];
try {
const querySnapshot = await getDocs(collection(db, resource));
querySnapshot.forEach((doc) => {
let docData = doc.data();
if (resource === "trips") {
docData.startDate = docData.startDate.toDate();
docData.endDate = docData.endDate.toDate();
}
list.push({ id: doc.id, ...docData });
});
setter(list);
} catch (error) {
console.log(error);
}
};
fetchData("trucks", setTruckData);
fetchData("facilities", setFacilityData);
fetchData("trips", setTripData);
}, []);
The app is at logi-dashboard, if that helps any.
EDIT Turns out the issue was with my hosting service, not the project. Go figure.
Based on my understanding, it seems like the facilityData on which you are trying to apply filter and which is coming from AppContext(Context hook variable) is found to be empty array when the useEffect code is getting executed, this might be scene if you are hitting any API to get the data into facility but the API response is not coming till the time useEffect is getting executed or any other source which is not populating the facilityData until useEffect runs.
In that case, you can add facilityData in the dependency array of useEffect, which will help the useEffect execute again once the facilityData is populated(updated)

How can i prevent useEffect() from firing up the first time but listening for a state change?

I'm using react, node express, postgres
I have a react component that is an html table that gets populated from a postgres table.
Here is parent component Materials:
const Materials = () => {
const [thickness1, setThickness] = useState(0);
const [width1, setWidth] = useState(0);
const [length1, setLength] = useState(0);
const [partTotalDemand, setTotalDemand] = useState(0);
const [partPlanned, setPlanned] = useState(0);
...
Here is a method in the component that retrieves data
// Material requirements calculation
const getReq = async (id) => {
try {
const response = await fetch(`http://localhost:5000/materials/${id}`, [id])
const jsonData = await response.json();
const tempThickness = jsonData.parts_material_thickness
const tempWidth = jsonData.parts_material_width
const tempLength = jsonData.parts_material_length
const tempTotalDemand = jsonData.workorder_total
const tempPlanned = jsonData.parts_produced
stateSetter(tempThickness, tempWidth, tempLength)
} catch (err) {
console.log(err.message);
}
}
I then want to update the states of the global constants:
const stateSetter = (thickness, width, length) => {
try {
setThickness(thickness);
setWidth(width);
setLength(length);
console.log(thickness1);
console.log(width1);
console.log(length1);
} catch (err) {
console.log(err.message)
}
}
useEffect(() => {
stateSetter();
}, [thickness1]);
Essentially the getReq() method is supposed to retrieve the information, and then I need to update the states with those values. As I understand I then need to re-render the component so the new states are usable. I attempted to do this via useEffect() but I'm not successful. The idea was to stop getReq() from firing up on the first render, but if the state changes for thickness1/width1/length1 then it should fire up and re-render, help much appreciated!
You're over-complicating this. All you need to do is set the state values:
const getReq = async (id) => {
try {
const response = await fetch(`http://localhost:5000/materials/${id}`, [id])
const jsonData = await response.json();
// set state values
setThickness(jsonData.parts_material_thickness);
setWidth(jsonData.parts_material_width);
setLength(jsonData.parts_material_length);
setTotalDemand(jsonData.workorder_total);
setPlanned(jsonData.parts_produced);
} catch (err) {
console.log(err.message);
}
}
You don't need to manually do anything to re-render the component. It will re-render whenever state is updated. So the "setter" functions being invoked here will trigger that re-render. (All of the state updates will be batched. So the above won't trigger 5 re-renders, just one with the 5 updated state values.)
Where you would use useEffect is when you want to have some logic which responds to a change in a particular state. For example, if you want to show a message every time thickness changes to a negative value, you'd do something like:
useEffect(() => {
if (thickness < 1) {
alert('negative thickness!');
}
}, [thickness]);
But that's not what you're doing here. All you're doing here is setting state values.

UseQuery graphql -> useState with useEffect such that I can run second useEffect to set more state

I am using Apollo for graphQL queries in a NextJS project I have the following request. I feel like the solution to this is simple, but the error occurs at the stateData.allProducts section it is saying it is null, but I have set the state in the useEffect and it has data as a dependency in the array, so shouldn't it re-render once data has loaded.
This is all works just fine if I placed these checks below a if statement checking the loading and return ...loading but then I can't use useEffect.
Any help as to what I am doing wrong would be greatly appreciated.
Thanks!
const { data, loading, error } = useQuery(QUERY);
const Router = useRouter();
// Creating state and setting it from query
const [stateData, setStateData] = useState(null);
const [disableAddToCart, setDisableAddToCart] = useState(false);
useEffect(() => {
setStateData(data);
}, [data]);
//~~~~~~~// <--- RIGHT HERE IS WHERE I GET THE NULL ERROR
const productFound = stateData.allProduct.find(
(product: any) => product.slug.current === Router.query.product
);
const currentItem = cartDetails[productFound.id];
useEffect((): void => {
console.log("currentItem", currentItem);
if (currentItem) {
if (currentItem.quantity > 0) {
setDisableAddToCart(true);
} else {
setDisableAddToCart(false);
}
}
}, [currentItem]);
As long as your query is loading, or if there is an error, the data variable from useQuery will be null.
Therefore you have to check for the loading to have finished and that no error has occurred. And/or for the data to be defined.
Also, stateData is unnecessary, because data is already a ready-to-use state variable.
const { data, loading, error } = useQuery(QUERY);
const Router = useRouter();
const [disableAddToCart, setDisableAddToCart] = useState(false);
let productFound;
let currentItem;
if(!loading && !error && data?.allProduct){
productFound = data.allProduct.find(
(product: any) => product.slug.current === Router.query.product
);
currentItem = cartDetails[productFound.id];
}
useEffect((): void => {
//since you are using typescript, you can use the optional chaining operator
if (currentItem?.quantity > 0) {
setDisableAddToCart(true);
} else {
setDisableAddToCart(false);
}
}, [currentItem]);

React - Refactoring logic with hooks

INTRODUCTION
I have a screen which makes some queries to my server:
Fetch user by username (when the user types something in the search input)
Fetch premium users (when screen mounts + pull to refresh)
Fetch young users (when screen mounts + pull to refresh)
I am thinking about moving this to a hook.
This is my current code:
function MyScreen() {
// Fetch by username
const [isSearching, setIsSearching] = useState([]);
const [searchedUsers, setSearchedUsers] = useState([]);
// Premium
const [isLoading, setIsLoading] = useState(true); <--- NOTE THIS
const [premiumUsers, setPremiumUsers] = useState([]);
// Young users
const [youngUsers, setYoungUsers] = useState([]);
/*
FIRST METHOD
*/
const searchUsersByUsername = async (limit = 20) => {
setIsSearching(true);
try {
const result = await api.users.searchUsersByUsername(username, limit);
setSearchedUsers(result);
} catch(err) {
console.log(err);
}
setIsSearching(false);
}
/*
SECOND METHOD
*/
const getPremiumUsers = async (limit = 10) => {
// Note: No loading here
try {
const result = await api.users.getPremiumUsers(limit);
setPremiumUsers(result);
} catch(err) {
console.log(err);
}
}
/*
THIRD METHOD
*/
const getYoungUsers = async (limit = 10) => {
// Note: No loading here
try {
const result = await api.users.getYoungUsers(limit);
setYoungUsersUsers(result);
} catch(err) {
console.log(err);
}
}
// Effects and rendering...
useEffect(() => {
(async () => {
const promises = [getPremiumUsers(), getYoungUsers()];
await Promise.all(promises).catch((err) => {
console.log(err))
});
setIsLoading(false); // <---- NOTE THIS
})();
}, []);
}
PROBLEM
I cannot use the typical useQuery hook, as I am using Firestore Callable Functions. So, the only way to make requests, is to call my api methods (api.users.searchUser...)
As you can see in the code, I have two types of loading indicators (two states):
isSearching (for the searching by username functionality)
isLoading (for fetching premium and young users in parallel)
How can I implement a reusable hook for this logic?
I mean, I need to abstract all this stuff, in order to be able to:
Search users by username in other screens
Search premium users (without fetching young users in parallel)
Search young users (without fetching premium users in parallel)
And, also, to get the loading status of the three queries.
Note: In my current screen, as I have said before, I am using "setIsLoading" for the young and premium users parallel fetching, but maybe (to be more flexible) in other screens I will need the loading status for each logic independently.
Any help or ideas?
You could use a React.useEffect and React.useCallback by each fetch method.
Also, save the loading status individually.
Check this out:
import { useEffect, useCallback } from 'react';
const defaultParams = {
fetchPremiumUsersOnMount: false,
fetchYoungUsersOnMount: false,
searchOnMount: false,
username: '',
limit: 20,
}
function useUsersApi(params = defaultParams) {
const [isSearching, setIsSearching] = useState(false);
const [searchedUsers, setSearchedUsers] = useState([]);
const [premiumUsers, setPremiumUsers] = useState([]);
const [premiumLoading, setPremiumLoading] = useState(false);
const [youngUsers, setYoungUsers] = useState([]);
const [youngLoading, setYoungLoading] = useState(false);
const fetchPremiumUsers = useCallback(async () => {
try {
setPremiumLoading(true);
const result = api.users.getPremiumUsers(params.limit);
setPremiumUsers(result);
} catch (err) {
console.log(err)
} finally {
setPremiumLoading(false);
}
}, [params.limit]);
const fetchYoungUsers = useCallback(async () => {
/* similar logic to `fetchPremiumUsers` */
}, [params.limit, params.]);
const fetchSearchUsers = useCallback(async (username) => {
/* fetch logic here */
}, [params.limit]);
useEffect(() => {
if(params.fetchPremiumUsersOnMount) {
fetchPremiumUsers();
}
}, [params.fetchPremiumUsersOnMount, params.limit]);
useEffect(() => {
if(params.fetchYoungUsersOnMount) {
fetchYoungUsers();
}
}, [params.fetchYoungUsersOnMount, params.limit]);
useEffect(() => {
if(params.fetchSearchUsers) {
fetchSearchUser(params.username);
}
}, [params.searchOnMount, params.limit, params.username]);
return {
isSearching,
searchedUsers,
isLoading: premiumLoading || youngLoading,
premiumUsers,
premiumUsersLoading: premiumLoading,
refreshPremiumUsers: fetchPremiumUsers,
youngUsers,
youngUsersLoading: youngLoading,
refreshYoungUsers: fetchYoungUsers,
}
}

How to put a dynamic data from firestore in the function where() and also use the snap.size to count the total query to be passed in a graph?

I have this data from firestore and I wanted to retrieve it dynamically with a where() but this is the error I'm getting:
TypeError: vaccines is not a function
The user collection:
[![enter image description here][1]][1]
Below are the codes:
const Vaccine = () => {
const [vaccines, setVaccines] = useState([]);
useEffect(() => {
const unsubscribe = firestore
.collection("vaccines")
.onSnapshot((snapshot) => {
const arr = [];
snapshot.forEach((doc) =>
arr.push({
...doc.data(),
id: doc.id,
})
);
setVaccines(arr);
});
return () => {
unsubscribe();
};
}, []);
Preface
As highlighted in the comments on the original question, this query structure is not advised as it requires read access to sensitive user data under /users that includes private medical data.
DO NOT USE THIS CODE IN A PRODUCTION/COMMERICAL ENVIRONMENT. Failure to heed this warning will lead to someone suing you for breaches of privacy regulations.
It is only suitable for a school project (although I would a fail a student for such a security hole) or proof of concept using mocked data. The code included below is provided for education purposes, to solve your specific query and to show strategies of handling dynamic queries in React.
From a performance standpoint, in the worst case scenario (a cache miss), you will be billed one read, for every user with at least one dose of any vaccine, on every refresh, for every viewing user. Even though your code doesn't use the contents of any user document, your code must download all of this data too because the Client SDKs do not support the select() operator.
For better security and performance, perform this logic server-side (e.g. Cloud Function, a script on your own computer, etc) and save the results to a single document that can be reused by all users. This will allow you to properly tighten access to /users. It also significantly simplifies the code you need to display the graphs and live statistics on the client-side.
useEffect
As stated by the React documentation on the Rules of hooks:
Only Call Hooks at the Top Level
Don’t call Hooks inside loops, conditions, or nested functions. Instead, always use Hooks at the top level of your React function, before any early returns. By following this rule, you ensure that Hooks are called in the same order each time a component renders. That’s what allows React to correctly preserve the state of Hooks between multiple useState and useEffect calls.
The documentation further elaborates that React relies on the order in which Hooks are called, which means that you can't have hook definitions behind conditional logic where their order and quantity changes between renders. If your hooks rely on some conditional logic, it must be defined inside of the hook's declaration.
As an example, if you have an effect that relies on other data, with this logic:
const [userProfile, setUserProfile] = useState();
const [userPosts, setUserPosts] = useState(null);
useEffect(() => {
// get user profile data and store in userProfile
}, []);
if (userProfile) {
useEffect(() => {
// get user post list and store in userPosts
}, [userProfile]);
}
you need to instead use:
const [userProfile, setUserProfile] = useState();
const [userPosts, setUserPosts] = useState(null);
useEffect(() => {
// get user profile data and store in userProfile
}, []);
useEffect(() => {
if (!userProfile) {
// not ready yet/signed out
setUserPosts(null);
return;
}
// get user post list and store in userPosts
}, [userProfile]);
Similarly, for arrays:
someArray && someArray.forEach((entry) => {
useEffect(() => {
// do something with entry to define the effect
}, /* variable change hooks */);
});
should instead be:
useEffect(() => {
if (!someArray) {
// not ready yet
return;
}
const cleanupFunctions = [];
someArray.forEach((entry) => {
// do something with entry to define an effect
cleanupFunctions.push(() => {
// clean up the effect
});
});
// return function to cleanup the effects created here
return () => {
cleanupFunctions.forEach(cleanup => cleanup());
}
}, /* variable change hooks */);
Because this looks a lot like lifecycle management, you are actually better off replacing it with nested components rather than using hooks, like so:
return (
<> // tip: React.Fragment shorthand (used for multiple top-level elements)
{
someArray && someArray
.map(entry => {
return <Entry key={entry.key} data={entry.data} />
})
}
</>
);
Adapting to your code
Note: The code here doesn't use onSnapshot for the statistics because it would cause a rerender every time a new user is added to the database.
const getVaccineStats = (vaccineName) => {
const baseQuery = firestore
.collection("users")
.where("doses.selectedVaccine", "==", vaccine);
const oneDoseQueryPromise = baseQuery
.where("doses.dose1", "==", true)
.where("doses.dose2", "==", false)
.get()
.then(querySnapshot => querySnapshot.size);
const twoDoseQueryPromise = baseQuery
.where("doses.dose1", "==", true)
.where("doses.dose2", "==", true)
.get()
.then(querySnapshot => querySnapshot.size);
return Promise.all([oneDoseQueryPromise, twoDoseQueryPromise])
.then(([oneDoseCount, twoDoseCount]) => ({ // tip: used "destructuring syntax" instead of `results[0]` and `results[1]`
withOneDose: oneDoseCount,
withTwoDoses: twoDoseCount
}));
};
const Vaccine = () => {
const [vaccines, setVaccines] = useState();
const [vaccineStatsArr, setVaccineStatsArr] = useState([]);
// Purpose: Collect vaccine definitions and store in `vaccines`
useEffect(() => {
return firestore // tip: you can return the unsubscribe function from `onSnapshot` directly
.collection("vaccines")
.onSnapshot({ // tip: using the Observer-like syntax, allows you to handle errors
next: (querySnapshot) => {
const vaccineData = []; // tip: renamed `arr` to indicate what the data contains
querySnapshot.forEach((doc) =>
vaccineData.push({
...doc.data(),
id: doc.id,
});
);
setVaccines(vaccineData);
}),
error: (err) => {
// TODO: Handle database errors (e.g. no permission, no connection)
}
});
}, []);
// Purpose: For each vaccine definition, fetch relevant statistics
// and store in `vaccineStatsArr`
useEffect(() => {
if (!vaccines || vaccines.length === 0) {
return; // no definitions ready, exit early
}
const getVaccineStatsPromises = vaccines
.map(({ vaccine }) => [vaccine, getVaccineStats(vaccine)]);
// tip: used "destructuring syntax" on above line
// (same as `.map(vaccineInfo => [vaccineInfo.vaccine, getVaccineStats(vaccineInfo.vaccine)]);`)
let unsubscribed = false;
Promise.all(getVaccineStatsPromises)
.then(newVaccineStatsArr => {
if (unsubscribed) return; // unsubscribed? do nothing
setVaccineStatsArr(newVaccineStatsArr);
})
.catch(err => {
if (unsubscribed) return; // unsubscribed? do nothing
// TODO: handle errors
});
return () => unsubscribed = true;
}, [vaccines]);
if (!vaccines) // not ready? hide element
return null;
if (vaccines.length === 0) // no vaccines found? show error
return (<span class="error">No vaccines found in database</span>);
if (vaccineStatsArr.length === 0) // no stats yet? show loading message
return (<span>Loading statistics...</span>);
return (<> // tip: React.Fragment shorthand
{
vaccineStatsArr.map(([name, stats]) => {
// this is an example component, find something suitable
// the `key` property is required
return (<BarGraph
key={name}
title={`${name} Statistics`}
columns={["One Dose", "Two Doses"]}
data={[stats.withOneDose, stats.withTwoDoses]}
/>);
});
}
</>);
};
export default Vaccine;
Live Statistics
If you want your graphs to be updated live, you need "zip together" the two snapshot listeners into one, similar to the rxjs combineLatest operator. Here is an example implementation of this:
const onVaccineStatsSnapshot => (vaccine, observerOrSnapshotCallback, errorCallback = undefined) => {
const observer = typeof observerOrCallback === 'function'
? { next: observerOrSnapshotCallback, error: errorCallback }
: observerOrSnapshotCallback;
let latestWithOneDose,
latestWithTwoDoses,
oneDoseReady = false,
twoDosesReady = false;
const fireNext = () => {
// don't actually fire event until both counts have come in
if (oneDoseReady && twoDosesReady) {
observer.next({
withOneDose: latestWithOneDose,
withTwoDoses: latestWithTwoDoses
});
}
};
const fireError = observer.error || (err) => console.error(err);
const oneDoseUnsubscribe = baseQuery
.where("doses.dose1", "==", true)
.where("doses.dose2", "==", false)
.onSnapshot({
next: (querySnapshot) => {
latestWithOneDose = querySnapshot.size;
oneDoseReady = true;
fireNext();
},
error: fireError
});
const twoDoseUnsubscribe = baseQuery
.where("doses.dose1", "==", true)
.where("doses.dose2", "==", true)
.onSnapshot({
next: (querySnapshot) => {
latestWithTwoDoses = querySnapshot.size;
twoDosesReady = true;
fireNext();
},
error: fireError
});
return () => {
oneDoseUnsubscribe();
twoDoseUnsubscribe();
};
}
You could rewrite the above function to make use of useState, but this would unnecessarily cause components to rerender when they don't need to.
Usage (direct):
const unsubscribe = onVaccineStatsSnapshot(vaccineName, {
next: (statsSnapshot) => {
// do something with { withOneDose, withTwoDoses } object
},
error: (err) => {
// TODO: error handling
}
);
or
const unsubscribe = onVaccineStatsSnapshot(vaccineName, (statsSnapshot) => {
// do something with { withOneDose, withTwoDoses } object
});
Usage (as a component):
const VaccineStatsGraph = (vaccineName) => {
const [stats, setStats] = useState(null);
useEffect(() => onVaccineStatsSnapshot(vaccineName, {
next: (newStats) => setStats(newStats),
error: (err) => {
// TODO: Handle errors
}
}, [vaccineName]);
if (!stats)
return (<span>Loading graph for {vaccineName}...</span>);
return (
<BarGraph
title={`${name} Statistics`}
columns={["One Dose", "Two Doses"]}
data={[stats.withOneDose, stats.withTwoDoses]}
/>
);
}
vaccines is an array and not a function. You are trying to run a map on vaccines. Try refactoring your code to this:
vaccines &&
vaccines.map((v, index) => {
// ...
})
Also do check: How to call an async function inside a UseEffect() in React?
here is the code, that works for you:
function DatafromFB() {
const[users, setUsers] = useState({});
useEffect(()=>{
const fetchVaccine = async () => {
try {
const docs = await db.collection("vaccines").get();;
docs.forEach((doc) => {
doc.data().vaccineDetails
.forEach(vaccineData=>{
fetchUsers(vaccineData.vaccine)
})
})
} catch (error) {
console.log("error", error);
}
}
const fetchUsers = async (vaccine)=>{
try {
const docs = await db.collection("users")
.where("doses.selectedVaccine", "==", vaccine).get();
docs.forEach(doc=>{
console.log(doc.data())
setUsers(doc.data());
})
}catch(error){
console.log("error", error);
}
}
fetchVaccine();
},[])
return (
<div>
<h1>{users?.doses?.selectedVaccine}</h1>
</div>
)
}
export default DatafromFB
what is ${index.vaccine} I think it must be v.vaccine
also setSize(snap.size); will set set size commonly not vaccine specific

Categories

Resources