Hello trying to make a conversations app based on twilio
The app is doing fine but everytime someone clicks a Conversation from the Conversations List, it starts a useEffect, the useEffect has something like the following function:
const setChannelEvents = useCallback((channel) => {
channel.on('messageAdded', (message) => {
setMessages(prevMessages => [...message, prevMessages])
})
The problem is that every time we leave the conversation that listener stays on so every time someone clicks on a conversation the app is creating a loot of listeners and well that isn't cool.
Wondering how to turn off the listener when it leaves the component
Tried something like this:
useEffect(() => {
Twilio.getClient()
.then((client) => client.getConversationBySid(channelId))
.then((channel) => setChannelEvents(channel))
.catch((err) => console.log('err', err.message))
.finally(() => setLoading(''))
return () => chatClientChannel.current.removeListener('messageAdded')
}, [channelId, setChannelEvents])
Didnt work
Any help would be appreciated, Thanks :)
Twilio developer evangelist here.
I think your useEffect is mostly right, but you are potentially using a different object in the return clause to the original channel. The beauty of a React hook like useEffect is that you can encapsulate some of the state in the function, and then use that in the tear down.
Try this instead:
const handleMessageAdded = (message) => {
// do something with the new message
}
useEffect(() => {
let currentChannel;
Twilio.getClient()
.then((client) => client.getConversationBySid(channelId))
.then((channel) => {
currentChannel = channel;
return setChannelEvents(channel);
})
.catch((err) => console.log('err', err.message))
.finally(() => setLoading(''))
return () => {
if (currentChannel) {
currentChannel.removeListener('messageAdded', handleMessageAdded);
}
};
}, [channelId, setChannelEvents]);
Here you set the currentChannel (I'm using let to define the variable and then updating it once the promise resolves. You could also do this by breaking your promise chain up into awaited functions that set the currentChannel) in the useEffect function, then remove the listener from that same object in the tear down function.
Related
bellow I have the standard way of setting the user state to currentuser that is returned from onAuthStateChanged. I would like a useEffect hook that is called when any update to user's info is made. I cant seam to figure out how to do that.
My current solution is to manually trigger a function that sets checkingStatus to true so that the bellow useEffect runs again.
useEffect(() => {
console.log("change")
const unsubscribe = onAuthStateChanged(auth, (currentuser) => {
setUser(currentuser);
setCheckingStatus(false);
});
return () => {
unsubscribe();
};
}, [aucheckingStatus]);
Is there a way to listen to any changes made to user and then trigger a useEffect that updates the user state ?
While auth.currentUser actually contains up to date user data, for reasons beyond my comprehension you cant actually trigger a useEffect using auth.currentUser because ReactJS is not recognising the change, also setting the user state to auth.currentUser will also not work, but for some reason if you spread auth.currentUser React will properly update state.
As a result my solution is to spread auth.currentUser after I finish a profile update or other user data related actions.
const updateUserProfile = (profileData) => {
updateProfile(auth.currentUser, profileData).then((res) => {
// Success
setUser({...auth.currentUser})
}).catch((error) => {
// Fail
})
}
useEffect(() => {
const unsubscribe = onAuthStateChanged(auth, (currentuser) => {
setUser(currentuser);
});
return () => {
unsubscribe();
};
}, []);
I am following a react-native tutorial but am having some trouble with React contexts and ActivityIndicator, I dont know where the problem lies, but I will try to be as descriptive as possible.
The problem:-
The code :
I am using contexts to provide the app with the location that has been searched and then searching for that location within my mock data, later returning the restaurants around that location.
complete source code at https://github.com/diivi/KiloBite/blob/main/src/services/location/location.context.js
Here I am using the onSearch function and passing it as a context prop to my search box to use with onSubmitEditing.
In your code you call onSearch and there you setILoading(true)
and setKeyword(searchKeyword).
Then in the useEffect you use the keyword you set in onSearch. Your useEffect runs only in keyword changes (see dependencies).
Try to add onSearch in your dependencies (look below).
Or maybe even locationRequest, locationTransform.
I would also try setIsLoading and generally try to put as dependencies everything you use in your useEffect.
const onSearch = (searchKeyword) => {
setIsLoading(true);
setKeyword(searchKeyword);
};
useEffect(() => {
if (!keyword.length) {
return;
}
locationRequest(keyword.toLowerCase())
.then(locationTransform)
.then((result) => {
setIsLoading(false);
setLocation(result);
})
.catch((err) => {
setIsLoading(false);
setError(err);
});
}, [keyword, onSearch]);
BUT, at the end I wonder, why you use a useEffect?
Why don't you just move all the code in onSearch:
const onSearch = (searchKeyword) => {
setIsLoading(true);
// setKeyword(searchKeyword); // not needed
if (!searchKeyword.length) {
return;
}
locationRequest(searchKeyword.toLowerCase())
.then(locationTransform)
.then((result) => {
setIsLoading(false);
setLocation(result);
})
.catch((err) => {
setIsLoading(false);
setError(err);
});
};
I'd like to make my swr state mutations resilient against network errors and support some kind of retry functionality.
Basically what I do is inside my useSWR powered react hook:
const setFavourite = useCallback((objectId) => {
const url = `/favs/${objectId}`
const newData = [objectId, ...data]
return mutate(newData, false)
.then(() => request("POST", url))
.then(() => mutate())
.catch((error) => {
mutate(data, false).catch(() => {})
const userError = new Error("Could not add favourite")
userError.retry = () => setFavourite(objectId)
})
}, [data, mutate, request])
I generate a new Error object with a retry function that can be used by the UI code to show a toast to retry the operation at later time (upon user interaction).
I wonder if the dependencies to setFavourite are still valid at the time retry() would be called. I guess not, as they depend on data and if this changes, then the setFavourite is also regenerated.
Anybody implemented such kind of retry before?
I thought I had a simple function:
database change trigger (.onUpdate)
find out which change is possibly important for a notification (prepareNotifications(change))
ask firebase if there are records that want a notification about that change (getDatabaseNotifications(changeWithNotification))
sent notifications (sentNotifications(changeWithNotification))
I'm stuck for a couple off days now on how to resolve the Firebase call before moving on.
tried to Promise.all([getDatabaseNotifications()])
tried to chain this function like this:
changes
then firebase call
then sent notifiactions
What is happening:
I get the changes,
The call to Firebase is done,
But before waiting for the result it moves on to sending notifications.
It finds no notifications in Firebase (but there are notifications!)
It's gathering the notifications (array [])
... here I push a test notification ...
It's sending 1 notification (the test notification)
Then it resolves the Firebase notifications (this should be resolved before).
Then the function stops without doing anything anymore.
This is how my function looks now. Can someone explain how I can wait on Firebase?
exports.showUpdate = functions.firestore
.document('shows/{showId}')
.onUpdate((change, context) => {
return prepareNotifications(change) // check if and which notifications to get out of the firebase database
.then(changes => {
console.log('changes');
console.log(changes);
if(changes) {
const gatherData = [];
changes.forEach(change => {
console.log('change');
console.log(change);
getDatabaseNotifications(change.showId, change.ring, change.ringNumber) // firebase call
.then(data => {
gatherData.push([...gatherData, ...data]);
console.log('gatherData');
console.log(gatherData);
})
.catch(err => {
console.log(err);
})
})
return gatherData;
}
return null;
})
.then(notifications => {
console.log('notifications');
console.log(notifications);
notifications.push(testData); // add some test notifications
if (notifications && notifications.length > 0) {
sentNotifications(notifications); // sent notifications
return 'sending notifications';
}
return 'no notifications to sent';
})
.catch(err => {
Sentry.captureException(new Error(`Showupdate sending notifications not ok. Error message: ${err.message}`));
})
});
Updated code which works! thanks to your examples.
exports.showUpdate = functions.firestore
.document('shows/{showId}')
.onUpdate((change, context) => {
return prepareNotifications(change) // check if and which notifications to get out of the firebase database
.then(changes => {
if(changes) {
return getDbRecords(changes);
}
})
.then(notifications => {
if (notifications && notifications.length > 0) {
sentNotifications(notifications); // sent notifications
return 'sending notifications';
}
return 'no notifications to sent';
})
.catch(err => {
Sentry.captureException(new Error(`Showupdate sending notifications not ok. Error message: ${err.message}`));
})
});
function getDbRecords(changes) {
const gatherData = [];
const gatherDataPromises = [];
changes.forEach(change => {
gatherDataPromises.push(
getDatabaseNotifications(change.showId, change.ring, change.ringNumber) // firebase call
.then(data => {
gatherData.push(...data);
})
);
});
return Promise.all(gatherDataPromises)
.then(() => { return gatherData }
);
}
This section of your code doesn't handle promises properly, it creates a bunch of work but then will return gatherData before any of it has happened, which is why you don't see any notifications:
if(changes) {
const gatherData = [];
changes.forEach(change => {
console.log('change');
console.log(change);
getDatabaseNotifications(change.showId, change.ring, change.ringNumber) // firebase call
.then(data => {
gatherData.push([...gatherData, ...data]);
console.log('gatherData');
console.log(gatherData);
})
.catch(err => {
console.log(err);
})
})
return gatherData;
}
Notably, you probably want that return gatherData to be chained off the set of promises that are generated by the entire set of calls to getDatabaseNotifications.
Something like:
if(changes) {
const gatherData = [];
const gatherDataPromises = [];
changes.forEach(change => {
console.log('change');
console.log(change);
gatherDataPromises.push(
getDatabaseNotifications(change.showId, change.ring, change.ringNumber) // firebase call
.then(data => {
gatherData.push([...gatherData, ...data]);
console.log('gatherData');
console.log(gatherData);
})
);
});
return Promise.all(gatherDataPromises)
.then(() => { return gatherData });
}
I removed the catch statement to allow the error to bubble up to the top level catch.
Caution: I have not tested this, as I don't have sample data or the code for getDatabaseNotifications, but the general approach should solve your current problem. Likewise, it allows all the calls to getDatabaseNotifications to run in parallel, which should be significantly faster than just awaiting on them in sequence.
That said, you do have other problems in this code -- for example, the return null just below the block I am discussing will likely lead you into trouble when you try to use notifications.push() in the following then() (but this also appears to be test code).
I think it's because of the async nature of the methods. So, instead of waiting "getDatabaseNotifications()" to finish it's job, it jumps into ".then(notifications =>{}" and in this case gatherData returns empty.
putting await before calling the method might work.
await getDatabaseNotifications(change.showId, change.ring, change.ringNumber)
I have an issue where I am trying to use the Redux state to halt the execution of some polling by using the state in an if conditional. I have gone through posts of SO and blogs but none deal with my issue, unfortunately. I have checked that I am using mapStateToProps correctly, I update state immutably, and I am using Redux-Thunk for async actions. Some posts I have looked at are:
Component not receiving new props
React componentDidUpdate not receiving latest props
Redux store updates successfully, but component's mapStateToProps receiving old state
I was kindly helped with the polling methodology in this post:Incorporating async actions, promise.then() and recursive setTimeout whilst avoiding "deferred antipattern" but I wanted to use the redux-state as a single source of truth, but perhaps this is not possible in my use-case.
I have trimmed down the code for readability of the actual issue to only include relevant aspects as I have a large amount of code. I am happy to post it all but wanted to keep the question as lean as possible.
Loader.js
import { connect } from 'react-redux';
import { delay } from '../../shared/utility'
import * as actions from '../../store/actions/index';
const Loader = (props) => {
const pollDatabase = (jobId, pollFunction) => {
return delay(5000)
.then(pollFunction(jobId))
.catch(err => console.log("Failed in pollDatabase function. Error: ", err))
};
const pollUntilComplete = (jobId, pollFunction) => {
return pollDatabase(jobId, pollFunction)
.then(res => {
console.log(props.loadJobCompletionStatus) // <- always null
if (!props.loadJobCompletionStatus) { <-- This is always null which is the initial state in reducer
return pollUntilComplete(jobId, pollFunction);
}
})
.catch(err=>console.log("Failed in pollUntilComplete. Error: ", err));
};
const uploadHandler = () => {
...
const transferPromise = apiCall1() // Names changed to reduce code
.then(res=> {
return axios.post(api2url, res.data.id);
})
.then(postResponse=> {
return axios.put(api3url, file)
.then(()=>{
return instance.post(api3url, postResponse.data)
})
})
transferDataPromise.then((res) => {
return pollUntilComplete(res.data.job_id,
props.checkLoadTaskStatus)
})
.then(res => console.log("Task complete: ", res))
.catch(err => console.log("An error occurred: ", err))
}
return ( ...); //
const mapStateToProps = state => {
return {
datasets: state.datasets,
loadJobCompletionStatus: state.loadJobCompletionStatus,
loadJobErrorStatus: state.loadJobErrorStatus,
loadJobIsPolling: state.loadJobPollingFirestore
}
}
const mapDispatchToProps = dispatch => {
return {
checkLoadTaskStatus: (jobId) =>
dispatch(actions.loadTaskStatusInit(jobId))
};
};
export default connect(mapStateToProps, mapDispatchToProps)(DataLoader);
delay.js
export const delay = (millis) => {
return new Promise((resolve) => setTimeout(resolve, millis));
}
actions.js
...
export const loadTaskStatusInit = (jobId) => {
return dispatch => {
dispatch(loadTaskStatusStart()); //
const docRef = firestore.collection('coll').doc(jobId)
return docRef.get()
.then(jobData=>{
const completionStatus = jobData.data().complete;
const errorStatus = jobData.data().error;
dispatch(loadTaskStatusSuccess(completionStatus, errorStatus))
},
error => {
dispatch(loadTaskStatusFail(error));
})
};
}
It seems that when I console log the value of props.loadJobCompletionStatus is always null, which is the initial state of in my reducer. Using Redux-dev tools I see that the state does indeed update and all actions take place as I expected.
I initially had placed the props.loadJobCompletionStatus as an argument to pollDatabase and thought I had perhaps created a closure, and so I removed the arguments in the function definition so that the function would fetch the results from the "upper" levels of scope, hoping it would fetch the latest Redux state. I am unsure as to why I am left with a stale version of the state. This causes my if statement to always execute and thus I have infinite polling of the database.
Can anybody point out what might be causing this?
Thanks
I'm pretty sure this is because you are defining a closure in a function component, and thus the closure is capturing a reference to the existing props at the time the closure was defined. See Dan Abramov's extensive post "The Complete Guide to useEffect" to better understand how closures and function components relate to each other.
As alternatives, you could move the polling logic out of the component and execute it in a thunk (where it has access to getState()), or use the useRef() hook to have a mutable value that could be accessed over time (and potentially use a useEffect() to store the latest props value in that ref after each re-render). There are probably existing hooks available that would do something similar to that useRef() approach as well.