PReact useEffect doesn't trigger on state change - reactjs

I want to do a recursive function which basically runs each time a folder has a subfolder under it, so I can take all the content from all the subfolders available.
Not sure what I am missing here, but the state change of subFolders does not trigger the useEffect which have it as dependency:
const [imageList, setImageList] = useState([]) as any;
const [subFolders, setSubFolders] = useState([]) as any;
const getFilesFromFolder = (fileId: string) => {
let noToken = false;
const requestFunction = ((pageToken?: string) => {
gapi.client.drive.files.list({
q: `'${fileId}' in parents`,
pageToken
}).execute((res: any) => {
const token = res.nextPageToken && res.nextPageToken || null;
const images = res.files.filter((file: any ) =>
file.mimeType === 'image/jpeg' ||
file.mimeType === 'image/png' ||
file.mimeType === 'image/jpg'
);
setSubFolders([...subFolders, ...res.files.filter((file: any ) => file.mimeType === 'application/vnd.google-apps.folder')]);
setImageList([...imageList, ...images])
if (token) {
requestFunction(token);
} else {
noToken = true;
}
}).catch((err: any) => {
console.log('err', err)
})
});
if (!noToken) {
requestFunction();
}
}
useEffect(() => {
if (subFolders && subFolders.length > 0) {
subFolders.forEach((subFolder: any) => {
getFilesFromFolder(subFolder.id);
});
}
}, [subFolders])

Since you are basically looping over file structure and enqueueing state updates I'm going to just assume any issues you have are because you are using normal state updates. When these are enqueued in loops or multiple times within a render cycle they overwrite the previous enqueued update.
To resolve you should really use functional state updates. This is so each update correctly updates from the previous state, and not the state from the previous render cycle. It's a subtle but important difference and oft overlooked issue.
Functional Updates
setSubFolders(subFolders => [
...subFolders,
...res.files.filter((file: any ) =>
file.mimeType === 'application/vnd.google-apps.folder'
),
]);
setImageList(imageList => [...imageList, ...images]);

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

How to use useEffect correctly to apply modifications to the array state using Firestore?

I'm using React firebase to make a Slack like chat app. I am listening to the change of the state inside the useEffect on rendering. (dependency is []).
The problem I have here is, how to fire the changes when onSnapshot listener splits out the changed state. If change.type is "modified", I use modifyCandidate (which is an interim state) to save what's been updated, and hook this state in the second useEffect.
The problem of second effect is, without dependency of chats, which is the array of chat, there is no chat in chats (which is obviously true in the initial rendering). To get chats, I add another dependency to second effect. Now, other problem I get is whenever I face changes or addition to the database, the second effect is fired even if modification didn't take place.
How can I effectively execute second effect when only modification occurs as well as being able to track the changes of chats(from the beginning) or
am I doing something awkward in the listening phase?
Please share your thoughts! (:
useEffect(() => {
const chatRef = db.collection('chat').doc('room_' + channelId).collection('messages')
chatRef.orderBy("created").onSnapshot((snapshot) => {
snapshot.docChanges().forEach((change) => {
if (change.type === "added") {
console.log("New message: ", change.doc.data());
}
if (change.type === "modified") {
console.log("Modified message: ", change.doc.data());
setModifyCandidate(change.doc.data());
}
if (change.type === "removed") {
console.log("remove message: ", change.doc.data());
}
});
});
}, [])
useEffect(() => {
if(!modifyCandidate){
return
}
const copied = [...chats];
const index = copied.findIndex(chat => chat.id === modifyCandidate.id)
copied[index] = modifyCandidate
setChats(copied)
}, [modifyCandidate, chats])
initially, I also use this useEffect to load chats.
useEffect(() => {
const chatRef = db.collection('chat').doc('room_' + channelId).collection('messages')
chatRef.orderBy("created").get().then((snapshot) => {
const data = snapshot.docs.map((doc) => ({
id: doc.id,
...doc.data(),
}));
setChats(data);
})
}, [])
return <>
{
chats.map((chat) => {
return <div key={chat.id}>
<ChatCard chat={chat} users={users} uid={uid} index={chat.id} onEmojiClick={onEmojiClick}/>
</div>
})
}
</>
use useMemo instead of 2nd useEffect.
const chat = useMemo(() => {
if(!modifyCandidate){
return null
}
const copied = [...chats];
const index = copied.findIndex(chat => chat.id === modifyCandidate.id)
copied[index] = modifyCandidate
return copied
}, [modifyCandidate])

state hooks don't update from useEffect function calls

In useEffect I call getPins which queries a DB for pins. I am then looping through the pins and and trying to decrease 'pinsRemaining' which is a state hook.
In state it shows that the hook decreases, but it's not working in the code. When I console log pinsRemaining from getPinnedLocations() its always the same number.
The interesting thing is that if I make 'pinsRemaining' a const varaible (not state hook) it does work to decrease the counter, but that creates other problems.
I'm not sure how to get this code to work. I've read about how state hooks are async here
useState set method not reflecting change immediately
I've tried just adding an empty useEffect hook like this but it doesn't work
useEffect(() => {}, [pinsRemaining])
Maybe I need to be doing something inside of this?
Here is my full code.
const [pinsRemaining, setPinsRemaining] = useState(5)
useEffect(() => {
if (isLoggedIn && user !== {}) {
APIService.getAccountById(
Number(id)
).then(_user => {
setUser(_user)
getPins(_user)
}).catch(err => {
console.log("error", err)
})
}
}, []);
const getPins = (_user) => {
APIService.getPinsForUser(_user.id).then(pins => {
pins.map(pin => {
pushPinnedLocation(pin.location_id, _user)
})
})
}
const pushPinnedLocation = (location, date, _user) => {
//decrementing in hook doesn't work here
setPinsRemaining((prevState)=> prevState - 1)
//but decrementing with var does
pins = pins - 1
console.log("state hook", pinsRemaining)
console.log("const var", pins)
setLocationDateMap(new Map(locationDateMap.set(location, date)))
if(pinsRemaining === 0){
getMatchesFromDB(_user)
}
}
const getMatchesFromDB = (_user) => {
let pinsArray = Array.from(locationDateMap)
pinsArray.map(obj => {
let location = obj[0]
let date = obj[1]
let locationDate = date + "-" + location
let dbMatches;
let params = {
location_date_id: locationDate,
user_id: _user.id,
seeking: _user.seeking
}
APIService.getMatchesByLocationDateId(params).then(data => {
dbMatches = data.map((match, i) => {
if(match.likes_recieved && match.likes_sent
&& match.likes_recieved.includes(_user.id + "")
&& match.likes_sent.includes(_user.id + "")){
match.connectedStatus = 3
}else if(match.likes_recieved && match.likes_recieved.includes(_user.id + "")){
match.connectedStatus = 2
}else if(match.likes_sent && match.likes_sent.includes(_user.id + "")){
match.connectedStatus = 1
}else{
match.connectedStatus = 0
}
match.name = match.user_name
return match
})
}).then(() => {
setMatches(matches => [...matches, ...dbMatches]);
})
})
}

Trigger completeMethod in PrimeReact Autocomplete on callback

I am using primereact's Autocomplete component. The challenge is that I don't want to set the options array to the state when the component loads; but instead I fire an api call when the user has typed in the first 3 letters, and then set the response as the options array (This is because otherwise the array can be large, and I dont want to bloat the state memory).
const OriginAutocomplete = () => {
const [origins, setOrigins] = useState([]);
const [selectedOrigin, setSelectedOrigin] = useState(null);
const [filteredOrigins, setFilteredOrigins] = useState([]);
useEffect(() => {
if (!selectedOrigin || selectedOrigin.length < 3) {
setOrigins([]);
}
if (selectedOrigin && selectedOrigin.length === 3) {
getOrigins(selectedOrigin).then(origins => {
setOrigins([...origins]);
});
}
}, [selectedOrigin, setOrigins]);
const handleSelect = (e) => {
//update store
}
const searchOrigin = (e) => {
//filter logic based on e.query
}
return (
<>
<AutoComplete
value={selectedOrigin}
suggestions={ filteredOrigins }
completeMethod={searchOrigin}
field='code'
onChange={(e) => { setSelectedOrigin(e.value) }}
onSelect={(e) => { handleSelect(e) }}
className={'form-control'}
placeholder={'Origin'}
/>
</>
)
}
Now the problem is that the call is triggered when I type in 3 letters, but the options is listed only when I type in the 4th letter.
That would have been okay, infact I tried changing the code to fire the call when I type 2 letters; but then this works as expected only when I key in the 3rd letter after the api call has completed, ie., I type 2 letters, wait for the call to complete and then key in the 3rd letter.
How do I make the options to be displayed when the options array has changed?
I tried setting the filteredOrigins on callback
getOrigins(selectedOrigin).then(origins => {
setOrigins([...origins]);
setFilteredOrigins([...origins])
});
But it apparently doesn't seem to work.
Figured it out. Posting the answer in case someone ponders upon the same issue.
I moved the code inside useEffect into the searchOrigin function.
SO the searchOrigin functions goes like below:
const searchOrigin = (e) => {
const selectedOrigin = e.query;
if (!selectedOrigin || selectedOrigin.length === 2) {
setOrigins([]);
setFilteredOrigins([]);
}
if (selectedOrigin && selectedOrigin.length === 3) {
getOrigins(selectedOrigin).then(origins => {
setOrigins([...origins]);
setFilteredOrigins(origins);
});
}
if (selectedOrigin && selectedOrigin.length > 3) {
const filteredOrigins = (origins && origins.length) ? origins.filter((origin) => {
return origin.code
.toLowerCase()
.startsWith(e.query.toLowerCase()) ||
origin.name
.toLowerCase()
.startsWith(e.query.toLowerCase()) ||
origin.city
.toLowerCase()
.startsWith(e.query.toLowerCase())
}) : [];
setFilteredOrigins(filteredOrigins);
}
}

react setting state in a condition inside useEffect

I'm trying to avoid showing an alert in React Native more than once.
To do that, I am trying to update the state inside a condition which is inside a useEffect:
const [permissionStatus, setPermissionStatus] = useState('');
const [permissionsAlertShown, setPermissionsAlertShown] = useState(false);
useEffect(() => {
function handleAppStateChange() {
if (
AppState.currentState === 'active' &&
permissionStatus === 'denied' &&
!permissionsAlertShown
) {
setPermissionsAlertShown(true);
Alert.alert(
...
);
}
}
AppState.addEventListener('change', handleAppStateChange);
}, [permissionStatus, permissionsAlertShown]);
My issue is that if I navigate away from my app and come back to it, AppState.currentState changes and since setPermissionsAlertShown(true) is ignored, I am shown the alert again.
How do I handle this situation?
The answer was to create a callback function that will remove the listener. I will share if someone else ever looks for this.
const [permissionStatus, setPermissionStatus] = useState('');
const [permissionsAlertShown, setPermissionsAlertShown] = useState(false);
const handleAppStateChange = useCallback(() => {
if (
AppState.currentState === 'active' &&
permissionStatus === 'denied' &&
!permissionsAlertShown
) {
setPermissionsAlertShown(true);
Alert.alert(
...
);
}
}, [permissionStatus, permissionsAlertShown]);
useEffect(() => {
AppState.addEventListener('change', handleAppStateChange);
return () => AppState.removeEventListener('change', handleAppStateChange);
}, [handleAppStateChange]);
You just need to persist the state, with asyncStorage or some other storage.
The ideal case is to use what persisted of your action (an permission enabled, an API data saved etc).

Resources