How to use a custom hook within reduce callback function - javascript

I have an array of 'property' objects. I'd like to reduce them to a single 'total monthly revenue' value. However, this requires a db call for each individual object.
The db call happens within a custom hook, using react-query. This doesn't work inside a useEffect hook because of the custom hook. I'm not sure how to structure this operation.
The gist:
const [properties, isLoading, isError, error] = useGetOwnedProperties()
const [totalRev, setTotalRev] = useState(0)
useEffect(() => {
if (!properties) {
setTotalRev(0)
} else {
setTotalRev(properties.reduce((acc, cur) => {
const [revenue, revLoading] = useMonthlyRevenue(cur) // Not sure where this should happen
return revenue + acc
}))
}
}, [properties, isLoading])

Related

I have an array of different IDs and I want to fetch data from each IDs . Is it possible?

I have an array of mongoDB ids.
const pId =  ['62b3968ad7cc2315f39450f3', '62b37f9b99b66e7287de2d44']
I used forEach to seperate the IDs like :
pId.forEach((item)=>{
console.log(item)
})
but I have a database(products) from where I want to fetch data from. So I tried
const [product, setProduct] = useState([{}]);
useEffect(() => {
pId?.forEach((item) => {
const getProduct = async () => {
try {
const res = await userRequest.get("/products/find/" + item)
setProduct(res.data)
} catch (err) {
console.log(err)
}
}
getProduct()
})
}, [pId])
I used useState[{}] because I want to collect the data in an array of objects.
I used useState[{}] because I want to collect the data in an array of objects.
Your code isn't collecting objects into an array. It's setting each result of the query as the single state item (overwriting previous ones).
If you want to get all of them as an array, build an array; one way to do that is map with the map callback providing a promise of each element, then use Promise.all to wait for all of those promises to settle:
// The usual advice is to use plurals for arrays, not singulars ("products", not "product")
const [products, setProducts] = useState([]); // Start with an empty array
useEffect(() => {
if (pId) {
Promise.all(pId.map((item) => userRequest.get("/products/find/" + item)))
.then((products) => setProducts(products))
.catch((error) => console.log(error));
}
}, [pId]);
Note that if pId changes while one or more userRequest.get calls are still outstanding, you'll get into a race condition. If userRequest.get provides a way to cancel in-flight calls (like fetch does), you'll want to use that to cancel the in-flight calls using a cleanup callback in the useEffect. For example, if userRequest.get accepted an AbortSignal instance (like the built-in fetch does), it would look like this:
const [products, setProducts] = useState([]);
useEffect(() => {
const controller = new AbortController();
const { signal } = controller;
if (pId) {
Promise.all(pId.map((item) => userRequest.get("/products/find/" + item, { signal })))
.then((products) => setProducts(products))
.catch((error) => {
if (!signal.aborted) {
console.log(error);
}
});
}
return () => {
controller.abort();
};
}, [pId]);
Again, that's conceptual; userRequest.get may not accept an AbortSignal, or may accept it differently; the goal there is to show how to cancel a previous request using a useEffect cleanup callback.
You can map through the ids and create a promise for each, then use Promise.all() and at last set the products state:
import React, {
useState,
useEffect
} from 'react'
const Example = () => {
const [products, setProducts] = useState([]);
const ids = ['62b3968ad7cc2315f39450f3', '62b37f9b99b66e7287de2d44']
useEffect(() => {
if(ids) Promise.all(ids.map(id => userRequest.get("/products/find/" + id).then(r => r.data))).then(results => setProducts(results))
}, [ids])
}
I renamed some of the variables for clarity to the future visitors. (pId to ids and product to products).
You're overwriting your product array with an individual result from each request. A simple solution would be to append to the array instead:
setProduct(product => [...product, res.data]); // take the old array and append the new item
As T.J. Crowder rightly suggested in the comments, you would keep appending to the initial product state when using the simple setter, so you need to use callback form which receives the current state as a parameter and returns the update.
I suggest you rename that particular state to products/setProducts to make clear it's an array.
Not directly related to the question, but bear in mind that firing a huge number of individual requests may cause performance degradation on the client and backend; there are plenty of options to deal with that, so I am not going into more detail here.
yes, it's possible. just change your code a bit:
const [product, setProduct] = useState([]); // empty array is enough for initialzing
useEffect(() => {
async function doSomethingAsync() {
if(!pId) return;
let newArray = [];
for(let item of pId) {
try {
const res = await userRequest.get("/products/find/" + item)
newArray.push(res.data); // push data to temporary array
} catch (err) {
console.log(err)
}
}
// set new state once
setProduct(newArray);
}
doSomethingAsync();
}, [pId])

Use firebase onSnapshot() in for loop?

The below code works for getting my data from firestore. I'm trying to update this to use onSnapshot() instead of of get(). Maybe the core of my confusion is onSnapshot() doesn't return a promise and I've tried just adding the listeners into an array but it seems the data doesn't get updated. How do I iterate over a for loop of onSnapshot()'s and render the results?
const [activityDataArray, setActivityDataArray] = useState([]);
const userActivityIds = userData.activities
useEffect(() => {
let promises = [];
for (const activityId of userActivityIds) {
promises.push(getFirestoreData("activities", activityId));
}
Promise.all(promises).then(response => setActivityDataArray(response));
}, [userActivityIds]);
UPDATED CODE:
When I console.log() the array it has my data, but I think this is a trick with chrome dev tools showing new information. I think when I call setActivityDataArray it's running it on an empty array and then it never gets called again. So the data doesn't render unless I switch to a different tab in my application and go back. Then it renders correctly (so I know the data is good, it's just a rendering issue). I think I need to re-render within onSnapshot() but how do I do this correctly?
const [activityDataArray, setActivityDataArray] = useState<any>([]);
const userActivityIds: string[] = userData.activities
useEffect(() => {
let activityDataArrayDummy: any[] = []
for (const i in userActivityIds) {
firebase.firestore().collection("activities").doc(userActivityIds[i])
.onSnapshot((doc) => {
activityDataArrayDummy[i] = doc.data();
});
}
console.log("activityDataArrayDummy", activityDataArrayDummy)
setActivityDataArray(activityDataArrayDummy);
}, [userActivityIds]);
Simply calling onSnapshot() in a loop should do it.
import { doc, onSnapshot } from "firebase/firestore";
for (const activityId of userActivityIds) {
// get reference to document
const docRef = doc(db, "activities", activityId)
onSnapshot(docRef, (snapshot) => {
// read and render data from snapshot
})
}
However, if you ever need to unsubscribe from any of the listeners then you might have to store the Unsubscribe function returned by onSnapshot
somewhere in state.
Just in case you have 10 or less items in userActivityIds then you can use onSnapshot() with a Query instead:
const q = query(collection(db, "activities"), where(documentId(), "in", userActivityIds));
onSnapshot(q, (querySnapshot) => {
// ...
})
I went with this. Apparently state in react isn't guaranteed to be updated, so the proper way to setState with updated data is using a callback function.
https://reactjs.org/docs/state-and-lifecycle.html#state-updates-may-be-asynchronous
const [activityDataObj, setActivityDataObj] = useState({})
const userActivityIds: string[] = userData.activities
useEffect(() => {
for (const i in userActivityIds) {
firebase.firestore().collection("activities").doc(userActivityIds[i])
.onSnapshot((doc) => {
setActivityDataObj((activityDataObj) => {
return {...activityDataObj, [userActivityIds[i]]: doc.data()}
})
});
}
}, [userActivityIds]);

How are you supposed to fire useEffect with more complex dependencies?

I have encountered the need to be able to add arrays and objects to my useEffect dependency array. What are the recommended approaches for doing this? For arrays I currently use the length of the array which is flawed because an update to the array where its length was constant would not change.
For complex objects too large to recreate with useMemo I am currently using the package useDeepEffect which is doing deep comparisons on the object. I have seen mention of converting it to JSON which is anathema to me. All of my implementations seem slightly hacky here please advise on some recommended way as I have yet encountered any tutorial with state more complex than a counter.
Using JSON.stringify():
const [items, setItems] = useState({id: 0, content: ""});
useEffect(() => {
const getItems = async () => {
const res = await axios.get(apiURL);
if (res && res.data) {
setItems(res.data);
}
}
getItems();
}, [JSON.stringify(items)]);
Using deep comparison with lodash:
// For putting object array in dependency array
function deepCompareEquals(prevVal, currentVal){
return _.isEqual(prevVal,currentVal );
}
function useDeepCompareWithRef(value) {
const ref = useRef();
if (!deepCompareEquals(value, ref.current)) {
ref.current = value; //ref.current contains the previous object value
}
return ref.current;
}
useEffect(() => {
const getItems = async () => {
const res = await axios.get(apiURL + '/items');
if (res && res.data) {
setTodos(res.data);
}
}
getItems();
}, [useDeepCompareWithRef(items)]);
For a more thorough discussion, read here: https://betterprogramming.pub/tips-for-using-reacts-useeffect-effectively-dfe6ae951421

How to set multiple states using one response from axios API call in React

I invoke a GET API using axios in React. Backend (Python-Flask) is returning data by writing return jsonify(animals=animals, cars=cars) and these (cars and animals) are arrays.
The response has data in the following format:
{
"animals": ["elephant", "lion", "dog"],
"cars": ["range rover", "fortuner", "land cruiser"]
}
I want to update the cars state using the cars array and the animals state using the animals array. I tried the following but only animals state is updating, cars state is remaining empty
App
export default function App() {
const [animals, setAnimals] = useState([])
const [cars, setCars] = useState([])
useEffect(()=> {
axios.get("http://127.0.0.1:5000/animals_cars").then(
response=> setAnimals(response.data.animals),
resp => setCars(resp.data.cars)
);
console.log(cars)
console.log(animals)
}, []);
}
Any help on the responses, that is where I have no idea how can I split the response to update different states.
.then() takes up to two arguments; callback functions for the success and failure cases of the Promise. In this case, your second callback (resp => setCars(resp.data.cars)) is only called if the Promise is rejected.
You want to use the first callback which is called if the Promise is fulfilled. You can update two state variables in one single function like this:
export default function App() {
const [animals, setAnimals] = useState([]);
const [cars, setCars] = useState([]);
useEffect(() => {
axios.get('http://127.0.0.1:5000/animals_cars')
.then((response) => {
setCars(response.data.animals);
setAnimals(response.data.cars);
});
// state updates are asynchronous
// state is not updated yet, you can't log these
// well, you can but chances are it's the default value []
console.log(cars);
console.log(animals);
}, []);
}
Please note though that useState is asynchronous. You can't update the state on one line and assume it's already changed on the next one. You'll likely log the unchanged state.
You need an async function when you are loading data from an API to wait for the completion of the function. To do that in a useEffect you need to define a new function and execute it only when a condition is met. In this case I create a new boolean constant loading, I check against it's value and if it's true I execute the load function. Upon completion of the function I set loading to false and that would prevent the load function from executing again. This has the advantage that if you want to fetch your data again all you have to do is to set loading to true.
Another way to do that is to define a memoised load function with useCallback outside of the useEffect but I don't want to complicate things for you.
A clean way to write that is the following.
export default function App() {
const [loading, setLoading] = useState(true);
const [animals, setAnimals] = useState([])
const [cars, setCars] = useState([])
useEffect(()=> {
const load = async () => {
const responseData = await axios.get("http://127.0.0.1:5000/animals_cars")
.then((response) => {
return response.data;
})
.catch((error) => {
console.log(error);
});
setAnimals(responseData.animals);
setCars(responseData.cars);
setLoading(false);
}
if (loading) {
load();
}
},[loading]);
}
You Can hadle that with function like this
useEffect(() => {
axios.get("http://127.0.0.1:5000/animals_cars")
.then(function (response) {
// handle success
setAnimals(response.data.animals);
setCars(response.data.cars);
});
console.log(cars);
console.log(animals);
}, []);

How come my state isn't being filled with response?

console.log(data) gives me an object with the correct data but, I set rates to that data but when console logging rates I get an empty object {}. Thanks in advance.
const [rates, setRates] = useState({});
useEffect(() => {
search();
}, []);
const search = async () => {
const response = await axios.get('https://api.exchangeratesapi.io/latest');
const data = response.data.rates;
console.log(data);
setRates(data);
console.log(rates);
};
As someone said in the comment, state updates will be reflected in the next render. Also, there are some problems with your code I'll address.
const [rates, setRates] = useState({});
useEffect(() => {
// move this in here unless you plan to call it elsewhere
const search = async () => {
const response = await axios.get('https://api.exchangeratesapi.io/latest');
const data = response.data.rates;
setRates(data);
};
search();
}, [/* no dependencies means it runs once */]);
If you do plan on calling search elsewhere, wrap it in a useCallback hook so you can set it as a dependency of your useEffect (you'll get a lint warning without). useCallback with an empty dependency array will always return the same function reference, so your useEffect will only ever run the one time like it does now.
If you leave search as a normal function in the component, the reference changes each render, so your effect would run every render if you included it as a dependency.
const [rates, setRates] = useState({});
const searchCallback = useCallback(async () => {
const response = await axios.get('https://api.exchangeratesapi.io/latest');
const data = response.data.rates;
setRates(data);
}, [])
useEffect(() => {
// move this in here unless you plan to call it elsewhere
search();
}, [searchCallback]);

Categories

Resources