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

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

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 to use a custom hook within reduce callback function

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])

Trying to filter by dynamically added property/value in object array and save to state

I was wondering if anybody could help, I have a function that when it runs it will do a fetch and save the object array it gets to a variable, it will then loop through that object array and do another fetch and based on some data in that fetch it will add a new property/value to each object inside the array, I have that working fine, however I then want to filter the array by that new properties value and set the updated object array to state, the problem is the filter doesn't seem to be working, all I get back is the unfiltered data.
Code below, any help would be amazing.
import { useState, useEffect } from 'react'
import ResultsPage from './components/ResultsPage'
const App = () => {
const [results, setResults] = useState([])
const searchedRef = useRef()
useEffect(() => {
console.log(results)
}, [results])
const handleSearch = async () => {
let searched = searchedRef.current.value
let res = await fetch(`someAPI/${searched}`)
let data = await res.json()
searchResults = data
res = null
data = null
searchResults.map(async (element) => {
res = await fetch(`someAPI/${element}`)
data = await res.json()
element.name = data.name;
});
searchResults = searchResults.filter(element => element.name !== null);
setResults(searchResults)
}
return (
<>
<input type="text" ref={searchedRef} onKeyDown={handleSearch}/>
<ResultsPage results={results} />
</>
)
}
export default App
The map will not wait for each fetch to be done.
Async functions evaluate to Promises, so the mapping will evaluate to an array of Promises.
You need to use Promise.all to wait on an array of Promises.
await Promise.all(searchResults.map(/* ... */))
Example here.

React useEffect don't change state [duplicate]

This question already has answers here:
The useState set method is not reflecting a change immediately
(15 answers)
Closed 2 years ago.
I had read a lot of answers of questions about it, but it didn't helped me.
Functional component need once subscribe to events and get data from API to state.
But state don't changes correctly.
const [games, setGames] = useState(null);
const [activeGame, setActiveGame] = useState(null);
const [pending, setPending] = useState(false);
useEffect(() => {
if (games === null) {
setPending(true);
API.Game.get({}).then((res) => {
if (res.success) {
console.log(res.msg.games) // Returning array of games [{...}, {...}]
setGames(res.msg.games);
setActiveGame(res.msg.activeGame);
// Rendered games, but didn't changed state :/
setTimeout(() => console.log(games), 1000) // Returning null (initial state)
}
setPending(false);
});
API.Socket.emit('subscribe', 'game');
API.Socket.on('addGame', (game) => {
setGames((games) => [...games, game]);
});
API.Socket.on('open', (isReconnect) => {
if (isReconnect) API.Socket.emit('subscribe', 'game');
});
}
}, [games, pending, activeGame]);
Depending on answers you provided, I tried this, but it still don't work.
const [games, setGames] = useState([null]);
const [activeGame, setActiveGame] = useState(null);
const [pending, setPending] = useState(false);
const fetchGames = async () => {
setPending(true);
const res = await API.DurakGame.get({});
if (res.success) {
setGames(res.msg.games);
setActiveGame(res.msg.activeGame);
}
setPending(false);
return true;
};
useEffect(() => {
fetchGames();
API.Socket.emit('subscribe', 'durak');
API.Socket.on('addGame', (game) => {
setGames((games) => [...games, game]);
});
API.Socket.on('open', (isReconnect) => {
if (isReconnect) API.Socket.emit('subscribe', 'durak');
});
}, []);
Possibility that problem can be caused by :
Maybe API function that you is used is async ?
If yes, useEffect parameter callback is not async and can't be async maybe :D. Reference React Hooks.
Solution :
You can use sperate function API async and call on useEffect function.
Maybe you can refer on this link Example For useEffect async
The data state updated is Array types
State data type is immutable, that means you can't modify directly on the state (Different allocation memmory). And array type is one of data type that if the variable is assigmented on other can have different allocation. So you should be destruct the data for manipulating the data that can be same allocation.
React State with array
I hope that answered your question. Thanks before

Categories

Resources