React Map function iterates through an array only once - reactjs

I have an image input tag which is multiple. I want each image to be scaled down. I use the map function for this. I call this within a useEffect function. But now it is the case that the map function is only run through once, no matter how many images are in an array. How can I change this ?
const articelImg = (e) => {
if (e.target.files && e.target.files.length > 0) {
setFiles([...files, e.target.files]);
}
};
useEffect(() => {
files.length &&
files.map(async (file, idx) => { //Two objects, but only interred once
const thump = await thumpnail(file[idx]);
setThumpnails([...thumpnails, thump]);
});
}, [files]);

when you are working with async/await code in a loop best approach is to use for of loop, below is the code you can give it a try
const articelImg = (e) => {
if (e.target.files && e.target.files.length > 0) {
setFiles([...files, e.target.files]);
}
};
useEffect(() => {
(async () => if (files.length) {
for await (let file of files){
const thump = await thumpnail(file[idx]);
setThumpnails([...thumpnails, thump]);
}
})()
}, [files]);

You have probably got a stale state in this line setThumpnails([...thumpnails, thump]); because of async settings. Try this one setThumpnails(thumpnails => [...thumpnails, thump]); this will provide you a latest snapshot of state
or use refs as described in docs.

Related

Trying to query multiple documents from Firestore in React at once, using an array from the users profile

This is my current code:
useEffect(() => {
profile.familyCode.forEach((code) => {
console.log(code._id)
onSnapshot(query(collection(db, "group-posts", code._id, "posts"), orderBy("timestamp", "desc")
),
(querySnapshot) => {
const posts = querySnapshot.docs.map((doc) => ({
id: doc.id,
...doc.data(),
}));
setMessages([...messages, posts])
}
)
})
There are TWO code._id's and currently it is only setting my messages from one of them. What am I missing here?
Ive tried using some of firestores logical expressions to do the same thing with no success. This way I can at least pull some of them, but I would like to pull ALL of the posts from BOTH code._id's
You are missing the fact that setMessages is not updating messages itself immediately. So messages are closure-captured here with the old (or initial value) and calling setMessages will just overwrite what was previously set by previous onSnapshot.
Next issue - onSnapshot returns the unsubscribe function which should be called to stop the listener. Or you will get some bugs and memory leaks.
Here is a fast-written (and not really tested) example of possible solution, custom hook.
export function useProfileFamilyGroupPosts(profile) {
const [codeIds, setCodeIds] = useState([]);
const [messagesMap, setMessagesMap] = useState(new Map());
const messages = useMemo(() => {
if (!messagesMap || messagesMap.size === 0) return [];
// Note: might need some tweaks/fixes. Apply .flatMap if needed.
return Array.from(messagesMap).map(([k, v]) => v);
}, [messagesMap])
// extract codeIds only, some kind of optimization
useEffect(() => {
if (!profile?.familyCode) {
setCodeIds([]);
return;
}
const codes = profile.familyCode.map(x => x._id);
setCodeIds(curr => {
// primitive arrays comparison, replace if needed.
// if curr is same as codes array - return curr to prevent any future dependent useEffects executions
return curr.sort().toString() === codes.sort().toString() ? curr : codes;
})
}, [profile])
useEffect(() => {
if (!codeIds || codeIds.length === 0) {
setMessagesMap(new Map());
return;
}
const queries = codeIds.map(x => query(collection(db, "group-posts", x, "posts"), orderBy("timestamp", "desc")));
const unsubscribeFns = queries.map(x => {
return onSnapshot(x, (querySnapshot) => {
const posts = querySnapshot.docs.map((doc) => ({
id: doc.id,
...doc.data(),
}));
// update and re-set the Map object.
setMessagesMap(curr => {
curr.set(x, posts);
return new Map(curr)
})
});
});
// we need to unsubscribe to prevent memory leaks, etc
return () => {
unsubscribeFns.forEach(x => x());
// not sure if really needed
setMessagesMap(new Map());
}
}, [codeIds]);
return messages;
}
The idea is to have a Map (or just {} key-value object) to store data from snapshot listeners and then to flat that key-value to the resulting messages array. And to return those messages from hook.
Usage will be
const messages = useProfileFamilyGroupPosts(profile);

typeError: destroy is not a function nextjs

When I upgraded nextjs application from 9 to 12. There were some errors shown, that were not being taken take care of in previous version. One of them was: typeError: destroy is not a function
In the console I could see it mentioned next-dev.js?3515:25 Warning: useEffect must not return anything besides a function, which is used for clean-up. You returned null. If your effect does not require clean up, return undefined (or nothing
Not sure it was because of the update nextjs has become too strict during it's checking, but I will put it down the solution for myself and everyone.
In almost all of the cases this error occurs when you tried to return anything from your useEffect hook that is not a function.
The fault,
useEffect(() => someFunction());
or
useEffect(() => {
return someFunction();
});
The Fix,
useEffect(() => {
someFunction();
});
For more information read the following article,
https://typeofnan.dev/fix-uncaught-typeerror-destroy-is-not-a-function-in-react/
I also got the same issue, i was upgraded my Next App from v9 to v12. And i found it because the useEffect
My code before was like (my Next v9) =
useEffect(() => {
return () => {
removeEventListener("blur", updateWarning);
const inputFile = document.getElementById("input-file-ujian");
if (inputFile) {
inputFile.removeEventListener("click", (e) => {
window.removeEventListener("blur", updateWarning);
});
inputFile.removeEventListener("change", handleChange);
}
const videos = document.getElementsByClassName("note-video-clip");
for (let i = 0; i < videos.length; i++) {
videos[i].removeEventListener("mouseleave", () => {
window.addEventListener("blur", updateWarning);
});
videos[i].removeEventListener("mouseenter", () => {
window.removeEventListener("blur", updateWarning);
});
}
};
}, [pesertaUjian, warning]);
and this is my Next v12 (I remove the return code) =
useEffect(() => {
removeEventListener("blur", updateWarning);
const inputFile = document.getElementById("input-file-ujian");
if (inputFile) {
inputFile.removeEventListener("click", (e) => {
window.removeEventListener("blur", updateWarning);
});
inputFile.removeEventListener("change", handleChange);
}
const videos = document.getElementsByClassName("note-video-clip");
for (let i = 0; i < videos.length; i++) {
videos[i].removeEventListener("mouseleave", () => {
window.addEventListener("blur", updateWarning);
});
videos[i].removeEventListener("mouseenter", () => {
window.removeEventListener("blur", updateWarning);
});
}
}, [pesertaUjian, warning]);
I don't know why, I just remove all my return code in my useEffect and it's work for me
Update:
Update, i found that if you are using useEffect and async await. Don't use like it
useEffect(async() => {},[])
but you can create function async await outside the useEffect, for example
const yourFunction = async () => {}
useEffect(() => yourFunction(),[])
There were a lot of place in the code which I am maintining where useEffect was returning null like:
useEffect(() => {
if (variantSelected) {
const productViewTrackingTimeout = setTimeout(
useProductViewTracking({
...blah blah
}),
1000
);
return () => {
clearTimeout(productViewTrackingTimeout);
};
}
return null;
}, [variantSelected, productTitle, router]);```
I removed all return null values, and just putting a return works too. But not any value.

how to handle useRouter()s async problem in nextJS

When I click the Link in PublicMyTeam.js
it passes query(=el.group_name) using router.
// PublicMyTeam.js
<Link href={`/public/${el.group_name}`}>
<a><h1 className={styles.team_name}>{el.group_name}</h1></a>
</Link>
But because of async problem of useRouter(), an error occured in useEffect() in [group].js.
It can't read dbService.collection(group).
// [group].js
const router = useRouter()
const { group } = router.query
async function getGroupPlayers() {
const querySnapshot = await dbService.collection(group).doc('group_data').collection('players').get()
querySnapshot.forEach(doc => {
const singlePlayerObject = {
name: doc.data().name,
photoURL: doc.data().photoURL,
joined_date: doc.data().joined_date,
rating: doc.data().rating,
game_all: doc.data().game_all,
game_win: doc.data().game_win,
game_lose: doc.data().game_lose,
status: doc.data().status,
introduce: doc.data().introduce
}
setGroupPlayers(groupPlayers => [...groupPlayers, singlePlayerObject])
})
groupPlayers.sort((a, b) => b.rating - a.rating)
}
useEffect(() => {
getGroupPlayers()
}, [])
What should I do?
Try adding router to the useEffect's dependencies array, and check for router.query.group before calling getGroupPlayers().
useEffect(() => {
if (router.query.group) {
getGroupPlayers()
}
}, [router])
you could try putting the contents of the entire async function inside of useEffect and make the function self-invoking
useEffect(() => {
(async function Update() {
return (await page.current) === reviews_page
? true
: set_reviews_page((page.current = reviews_page));
})();
}, [page.current, reviews_page]);
this works quite well for custom pagination I have configured with SWR. It's my workaround for the same problem
If you want to read up on the topic, it's referred to as IIFEs (Immediately Invoked Function Expression)
So, something like this should do the trick:
useEffect(() => {
(async function getGroupPlayers() {
const querySnapshot = await dbService.collection(group).doc('group_data').collection('players').get()
querySnapshot.forEach(doc => {
const singlePlayerObject = {
name: doc.data().name,
photoURL: doc.data().photoURL,
joined_date: doc.data().joined_date,
rating: doc.data().rating,
game_all: doc.data().game_all,
game_win: doc.data().game_win,
game_lose: doc.data().game_lose,
status: doc.data().status,
introduce: doc.data().introduce
}
setGroupPlayers(groupPlayers => [...groupPlayers, singlePlayerObject])
})
groupPlayers.sort((a, b) => b.rating - a.rating)
})();
}, [])
Although, you may want to add some conditional logic for this to execute as intended (and a lifecycle dependency or two). make a useRef to reference group or groupPlayers and only trigger the lifecycle hook to execute on componentDidUpdate (similar to how I have the state updating when the page.current and reviews_page values differ). page.current is the useRef const for the reviews_page state as follows:
const [reviews_page, set_reviews_page] = useState<number>(1);
const page = useRef<number>(reviews_page);

react useState enters infinite loop even thou variable is the same

I have the following code:
const [ ddFilterData, setddFilterData ] = useState('');
useEffect(() => {
getDropdownData();
}, [ddFilterData]);
const getDropdownData = async () => {
if(optionDetails) {
let filteredData = Promise.all(
optionDetails.map(async (item, i) => {
const fltData = await filterData(item, props.items);
return fltData
})
)
filteredData.then(returnedData => {
setddFilterData(returnedData);
})
}
}
What I need is for useEffect to execute eah time ddFilerData changes with NEW or DIFFERENT data.
From my understanding it should only update or run when teh ddFilterData is different no?
Currently it runs on each change. The code above enters into an infinite loop even thou filteredData isn't different. Any ideas what I'm doing wrong?
Your returnedData is an array. So when you do setddFilterData(returnedData) you're setting a new value for ddFilterData. Because React uses Object.is for comparison, even if the array elements are the same as previously, it is still a different object and will trigger useEffect again, causing the infinite loop.
your getDropdownData method is updating ddFilterData which causes re-render. And on re-render you getDropdownData is called which updated ddFilterData due to this cyclic behavior your are getting infinte loop.
Modify your code like this:
const [ ddFilterData, setddFilterData ] = useState('');
useEffect(() => {
getDropdownData();
}, []);
useEffect(() => {
// put your code here if you want to do something on change of ddFilterData
}, [getDropdownData]);
const getDropdownData = async () => {
if(optionDetails) {
let filteredData = Promise.all(
optionDetails.map(async (item, i) => {
const fltData = await filterData(item, props.items);
return fltData
})
)
filteredData.then(returnedData => {
setddFilterData(returnedData);
})
}
}

React useEffect causing infinite loop even when dependencies are listed (with Firebase realtime database)

I've been trying to solve this but no matter what solution I do, it is still stuck in a infinite loop.
Here is the code
const [carr, setCarr] = useState({})
useEffect(() => {
sortedRosterCollection.once('value', (snap) => {
snap.forEach((doc) =>{
if (doc.key==="Carr Intermediate"){
var school = doc.key;
var mentorList = doc.val();
var schoolMentor = {school:school, mentors: mentorList};
setCarr(schoolMentor)
console.log(carr)
}
});
});
},[carr]);
No matter what I do "console.log(carr)" is fired endlessly.
If you want to inspect the value of carr whenever it's changed, put it into another use effect:
const [carr, setCarr] = useState({})
useEffect(() => {
sortedRosterCollection.once('value', (snap) => {
snap.forEach((doc) => {
if (doc.key === "Carr Intermediate") {
var school = doc.key;
var mentorList = doc.val();
var schoolMentor = {
school: school,
mentors: mentorList
};
setCarr(schoolMentor)
}
});
});
}, []);
useEffect(() => {
console.log(carr)
}, [carr])
You don't forget cleanup function in useEffect hook like this:
useEffect(() => {
effect
return () => {
cleanup
}
}, [input])
I used to make this problem like you because useEffect can compare two object.
You can make the reference to the link: https://medium.com/javascript-in-plain-english/comparing-objects-in-javascript-ce2dc1f3de7f. And I check changed by using below code:
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
}

Resources