I'm having a page with a textbox on (Textfield from fluent UI). I want to update a server stored value every time the user changes the text. I do not want the user to click a separate 'Save' button since that will interfer with the workflow on the page.
Everytime the user changes the text an event is raised with the new text. Simply enough I catch the event and updates the value. However, an event is raised for every character entered creating a lot of events. And if the user types quickly enough, the server will raise an error saying that I can't update the value cause there is already an update going on.
Is there a way to queue function calls? Meaning that I put the changes in a queue and a separate function handles them one after another?
<TextField
value={info.location}
onChange={locationChanged}
/>
const locationChanged = React.useCallback(
(event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>, newValue?: string) => {
// update server here
},
[]
);
You are probably looking for a debounce function. This function will cancel the call if time between previous call and current call is less than a given time.
function debounce(func, timeout = 300){
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => { func.apply(this, args); }, timeout);
};
}
const debounceChange = debounce(locationChanged);
<TextField
value={info.location}
onChange={debounceChange}
/>
Related
The getDocsRealtime() adds a Firebase listener for a query and returns an unsubscribe function. In componentDidMount, this function loads top 5 documents initially. Every time the page is scrolled to bottom, this function is executed again to get the next 5 documents, and returns another unsubscribe function for the new documents. I want to make it so that:
unsubscribe function = old unsubscribe function + new unsubscribe function
Here is my code:
componentDidMount = async () => {
let unsubscribe = await getDocsRealtime()
this.setState({ unsubscribe })
// Add Listener when page is scrolled to bottom
window.addEventListener('scroll', this.onScrollToBottom)
}
onScrollToBottom = async () => {
if (window.scrollY > placeholderTop) {
let unsubscribe = await getDocsRealtime()
unsubscribe = () => {
this.state.unsubscribe()
unsubscribe()
}
this.setState({ unsubscribe })
}
}
componentWillUnmount = () => {
// Unsubscribe Listener
this.state.unsubscribe && this.state.unsubscribe
// Remove Listener for when page is scrolled to bottom
window.removeEventListener('scroll', this.onScrollToBottom)
}
The problem I am having here is that instead of calling the old unsubscribe function, the program is calling the unsubscribe function stored in the state and I get RangeError: Maximum call stack size exceeded error.
Every time the page is scrolled to bottom, this function is executed
again to get the next 5 documents
Since you want to query the database each time a specific event occurs in your front-end, i.e. the scrolling to the bottom, you should not, IMO, use a listener to query the database.
I would suggest you paginate the queries by "combining query cursors with the limit() method" as explained in the doc. With this technique, each time the user scrolls down, you construct a new query starting at the last previously visible document and you execute it once (instead of continually "listening to it").
I understand that it seems interesting to combine realtime listening to the DB with pagination but in reality it can create weird situations when a document is shown twice in the front end because a new document has been added in the middle of a set of previously displayed docs.
I am using react app context to store an array of "alerts objects" which is basically any errors that might occur and I would want to show in the top right corner of the website. The issue I am having is that the context is not being up to date inside a timeout. What I have done for testing is gotten a button to add an alert object to the context when clicked and another component maps through that array in the context and renders them. I want them to disappear after 5 seconds so I have added a timeout which filters the item that got just added and removes it. The issue is that inside the timeout the context.alerts array seems to have the same value as 5 seconds ago instead of using the latest value leading to issues and elements not being filtered out. I am not sure if there's something wrong with my logic here or am I using the context for the wrong thing?
onClick={() => {
const errorPopup = getPopup(); // Get's the alert object I need
context.setAlerts([errorPopup, ...context.alerts]);
setTimeout(() => {
context.setAlerts([
...context.alerts.filter(
(element) => element.id !== errorPopup.id,
),
]);
}, 5000);
}}
onClick={() => {
const errorPopup = getPopup(); // Get's the alert object I need
context.setAlerts([errorPopup, ...context.alerts]);
setTimeout(() => {
context.setAlerts(alerts => [
...alerts.filter(
(element) => element.id !== errorPopup.id,
),
]);
}, 5000);
}}
This should fix it. Until react#17 the setStates in an event handler are batched ( in react#18 all setStates are batched even the async ones ), hence you need to use the most fresh state to make the update in second setAlerts.
To be safe it's a good practice using the cb syntax in the first setState as well.
I think the fix would be to move context.setAlerts(...) to a separate function (say removePopupFromContext(id:string)) and then call this function inside the setTimeout by passing the errorPopup.Id as parameter.
I'm not sure of your implementation of context.setAlerts, but if it's based on just setState function, then alternatively, you could do also something similar to how React let's you access prevState in setState using a function which will let you skip the creation of the extra function which may lightly translate to:
setContext(prevContextState =>({
...prevContextState,
alerts: prevContextState.alerts.filter(your condition)
)})
I'm creating a search function for a website that I'm working on. When a user types in a keyword, a get request will be sent to retrieve the matching information. However, it feels very wasteful to have it fire every time a key is pressed. Say the user wants to search for sandwiches, they will most likely enter that in pretty quick succession I just want to fire it after a certain amount of time after the user stopped typing(say 250ms). My idea is to set a timeout, which will be cleared on a consecutive keystroke. Unfortunately, the timeout does not reset and just ends up being delayed. I tried having it in a useEffect hook, and then the timeout worked fine but I had some other problems which prompted me to try and do it this way.
const onChangeBrand = (e) => {
const brand = e.target.value
setBrand(brand)
const timeout = setTimeout(()=>{
url.get(`brands?search=${encodeURI(brand.toLowerCase())}&rows=5`)
.then(res => {
setBrands(res.data)
if(res.data.length === 1 && res.data[0].urlEncoding === encodeURI(brand.toLowerCase())){
setPageType("models")
}else{
setPageType("brands")
}
})
},2500)
return () => clearTimeout(timeout);
}
Any help would be much appreciated!
You've got the right idea but the wrong execution. Returning a function from an onChange handler inherently does nothing–this would have worked fine with useEffect so I see where it came from. This pattern is known as throttling / debouncing a function and there are tons of premade libraries out there to help you throttle a function (like lodash.throttle) but it's perfectly cool to spin your own!
The key here would be:
Use a timeout variable that is scoped outside the method
At the start of execution of your onChange, check to see if the timeout variable has a value–if it does, clear it.
Execute onChange, assign new timeout.
You could use a ref or something here but I personally think it's easiest to define your timeout holder outside the scope of your component entirely.
let CHANGE_TIMEOUT = null;
function MyComponent(props) {
// .. component code
const onChangeBrand = (e) => {
if (CHANGE_TIMEOUT) {
// we already have a previous timeout, clear it.
clearTimeout(CHANGE_TIMEOUT);
}
const brand = e.target.value
setBrand(brand)
// Set the timeout again
CHANGE_TIMEOUT = setTimeout(()=>{
url.get(`brands?search=${encodeURI(brand.toLowerCase())}&rows=5`)
.then(res => {
setBrands(res.data)
if(res.data.length === 1 && res.data[0].urlEncoding === encodeURI(brand.toLowerCase())){
setPageType("models")
}else{
setPageType("brands")
}
})
},2500);
}
// .. other component code here
}
I'm developing a React app without Redux or any other state manager.
Let's say I want to do three things when a button is clicked:
Enable some other button
Remove a label
Show a confirmation toaster
These 3 things are controlled by 3 variables of the state. I could therefore do simply this:
myHandler = () => {
this.setState({
canSave: true, // Enable the button
isLabelVisible: false, // Hide label
isConfirmationMessageVisible: true, // Show confirmation message
});
}
However, I could get rid of those comments by using some private class functions, like this:
myHandler = () => {
this.toggleSaveButton(true);
this.toggleLabel(false);
this.toggleConfirmationMessage(true);
}
toggleSaveButton= (enabled) => {
this.setState({
canSave: enabled,
});
}
toggleLabel= (visible) => {
this.setState({
isLabelVisible: visible,
});
}
toggleConfirmationMessage= (visible) => {
this.setState({
isConfirmationMessageVisible: visible,
});
}
In addition to remove those comments which could easily get out-of-sync with the code, this allows me to reuse the private methods in other places of my code.
Since this is handled in a synthetic event, I have read here that it will be batched, so I can expect no performance penalty.
My question is: is this good practice? have you used this approach? can you point some potential drawbacks I can not foresee right now?
This is perfectly fine. As you mention, React batches all updates to the state that are triggered from event handlers. This means that you can safely use multiple setState() like you are doing here.
In current release, they will be batched together if you are inside a React event handler. React batches all setStates done during a React event handler, and applies them just before exiting its own browser event handler.
The only thing you need to look out for is if you are changing the same state twice from two setState() calls. For example:
a() {
this.setState({foo: foo+1});
}
b() {
this.setState({foo: foo+1});
}
Calling a() and then b() from the same event, will increment foo by 1, not two.
Instead use:
a() {
this.setState(prevState => ({foo: foo+1}));
}
b() {
this.setState(prevState => ({foo: foo+1}));
}
this will correctly increment foo by 2.
For potential future readers who are not calling multiple setState() from an event handler, I should note the following:
With current version, several setStates outside of event handlers (e.g. in network responses) will not be batched. So you would get two re-renders in that case.
Alternative solution
What you can do though, regardless if you call setState() from an event handler or not, is to build an object and then set it as the new state. The potential benefit here is that you only set the state once and thus don't rely on batching or where the function was triggered from (event handler or not).
So something like:
myHandler = () => {
let obj = {}
obj = this.toggleSaveButton(obj, true);
obj = this.toggleLabel(obj, false);
obj = this.toggleConfirmationMessage(obj, true);
this.setState(obj);
}
toggleSaveButton= (obj, enabled) => {
obj.canSave = enabled;
return obj;
}
toggleLabel= (visible) => {
obj.isLabelVisible = visible;
return obj;
}
toggleConfirmationMessage= (visible) => {
obj.isConfirmationMessageVisible = visible;
return obj;
}
I wonder if someone can explain the reason of this behavior:
If on a onChange event from an <input> element I have set to point to this method:
private PasswordChanged = (event: any) => {
this.setState((prevState: IWifiState, props: IWifiProps) => {
prevState.Password = event.target.value;
return prevState;
});
}
This throw me the following error:
Where line 27 is precisely the call to event.target.value on the pasted code.
If I change to code to be like that:
private PasswordChanged = (event: any) => {
const password = event.target.value;
this.setState((prevState: IWifiState, props: IWifiProps) => {
prevState.Password = password;
return prevState;
});
}
It just works as expected... Anyone can explain why?
Thanks!
React does something called Event Pooling.
What this essentially means is that, for performance considerations, they re-use events.
At the time when you call setState, internally the object might not be okay to re-use as it might behave in ways you wouldn't expect it to (properties get nulled out once the event has served it's purpose).
It is best to save off the reference in a variable for the value that you need, as you did, and use that instead.
Basically, you are accessing it asynchronously (inside the setState function) and it is advised against doing so.
There is a workaround, but I would also advise against it.
If you want to access the event properties in an asynchronous way, you should call event.persist() on the event, which will remove the synthetic event from the pool and allow references to the event to be retained by user code.