How do I sort of buffer my requests?
Currently I am using Material UI's slider in my React project. I am using the onChange property to trigger a http post requests when I move along the slider. The code looks something like this.
<Slider
onChange={handleChange}
valueLabelFormat={valueLabelFormat}
// aria-labelledby="discrete-slider"
aria-labelledby="non-linear-slider"
valueLabelDisplay="auto"
value={value}
min={1}
max={5}
step={0.05}
/>
However, if you can imagine, the onChange will be too frequent and there will be too many HTTP requests being sent. This is definitely not remended. I am also not intending to use onChangeCommitted as it is not my desired way of interaction.
So this is what I want to do:
IF I move along the slider (or onChange), onChange will continually update the value, but http requests will only be triggered every 500ms (with the latest value updated with onChange) . Rather than triggering a request for every onChange!
I have totally no idea how to achieve this..... setInterval or..? Not really sure. Help will be appreciated.
Thank you!
UPDATE
The answer is to use debounce (look at the answer below by k.s.)
However my use case is slightly different from the usual use cases. The function that I am using in my debounce requires inputs to it. I am using debounce inside my handleChange() function instead as I need the handleChange to set the value of my slider (so that it scrolls smoothly) and call debounce on the function inside that does the http calls.
<Slider
onChange={handleChange}
valueLabelFormat={valueLabelFormat}
// aria-labelledby="discrete-slider"
aria-labelledby="non-linear-slider"
valueLabelDisplay="auto"
value={value}
min={1}
max={5}
step={0.05}
/>
This is how my handleChange() looks like
const boostedLabel ="some label"
const handleChange = (_event, newValue) => {
//Update value back to Slider
setValue(newValue);
const debouncedFunc = () => updateWeightValue(boostedLabel, newValue);
debounce(debouncedFunc, 300, {leading:false, trailing:true});
};
However this does not work! The slider moves but it does not seem like the debouncedFunc is triggered at all.
How do I sort of buffer my requests?
Currently I am using Material UI's slider in my React project. I am using the onChange property to trigger a http post requests when I move along the slider. The code looks something like this.
<Slider
onChange={handleChange}
valueLabelFormat={valueLabelFormat}
// aria-labelledby="discrete-slider"
aria-labelledby="non-linear-slider"
valueLabelDisplay="auto"
value={value}
min={1}
max={5}
step={0.05}
/>
However, if you can imagine, the onChange will be too frequent and there will be too many HTTP requests being sent. This is definitely not remended. I am also not intending to use onChangeCommitted as it is not my desired way of interaction.
So this is what I want to do:
IF I move along the slider (or onChange), onChange will continually update the value, but http requests will only be triggered every 500ms (with the latest value updated with onChange) . Rather than triggering a request for every onChange!
I have totally no idea how to achieve this..... setInterval or..? Not really sure. Help will be appreciated.
Thank you!
UPDATE
The answer is to use debounce (look at the answer below by k.s.)
However my use case is slightly different from the usual use cases. The function that I am using in my debounce requires inputs to it. I am using debounce inside my handleChange() function instead as I need the handleChange to set the value of my slider (so that it scrolls smoothly) and call debounce on the function inside that does the http calls.
<Slider
onChange={handleChange}
valueLabelFormat={valueLabelFormat}
// aria-labelledby="discrete-slider"
aria-labelledby="non-linear-slider"
valueLabelDisplay="auto"
value={value}
min={1}
max={5}
step={0.05}
/>
This is how my handleChange() looks like
const boostedLabel ="some label"
const handleChange = (_event, newValue) => {
//Update value back to Slider
setValue(newValue);
const debouncedFunc = () => updateWeightValue(boostedLabel, newValue);
debounce(debouncedFunc, 300, {leading:false, trailing:true});
};
However this does not work! The slider moves but it does not seem like the debouncedFunc is triggered at all.
Share Improve this question edited Jun 23, 2020 at 2:02 Daryll asked Jun 22, 2020 at 20:30 Daryll Daryll 5713 gold badges9 silver badges16 bronze badges 2-
Hey, thanks for posting! Can you please show your current react code, specifically the
onChange
function. Thanks! – AttemptedMastery Commented Jun 22, 2020 at 20:36 - You can look into lodash debounce lodash./docs/4.17.15#debounce – SakoBu Commented Jun 22, 2020 at 20:41
2 Answers
Reset to default 6Your use case isn't notably different from other debounce use cases, you just aren't yet fully understanding how debounce
should be used. Calling debounce
does not invoke your function; instead it returns a debounced version of your function which can then be called as normal with however many parameters you need.
You should not be calling debounce
in handleChange
. You only want to call it once for the life of the element using it -- then you use the debounced version of the function (returned by debounce
) within your handleChange
function.
The simplest manner to do this is to create your debounced function at the top-level (entirely outside of your ponent). This isn't pletely safe, however, if it is possible for you to have more than one element on the page for that type of ponent. If you had multiple elements sharing the same debounced function, it would be possible (though unlikely) for a very fast user to change more than one slider before the end of the delay (e.g. within 300 milliseconds) in which case the first element changed would not get saved to the back-end. If the debounced function is actually saving the current state of all the editable elements on the page (and not only the one triggering the change event), then calling debounce at the top-level is what you want.
To avoid the slight dangers of a top-level debounce, you can call debounce in your ponent in a manner that ensures only calling it once for your element. For the 3rd slider in my example below, I'm calling debounce
in a lazy state initializer and ignoring the setter that is also returned by useState
. Then on each re-render, the same debounced function will be used. With slight changes, it would also be possible to leverage useRef
instead of useState
or useCallback
or useMemo
(though the guarantees of only calling debounce once per element are not as safe for useCallback
and useMemo
-- the documentation states "You may rely on useMemo as a performance optimization, not as a semantic guarantee.").
The example below shows 3 sliders (all sharing the same state). You can see that callHttpRequest
just logs its arguments to the console. Each of the 3 sliders calls this in their handleChange
functions in a different manner. The first is not debounced at all, so you can see many console logs as you move the slider. The second is debounced at the top-level, so you only see the console log when you stop moving the slider for at least 300 milliseconds. The third is debounced in the ponent within the lazy initializer passed to useState
and behaves the same as the second slider.
import React from "react";
import { makeStyles } from "@material-ui/core/styles";
import Grid from "@material-ui/core/Grid";
import Typography from "@material-ui/core/Typography";
import Slider from "@material-ui/core/Slider";
import VolumeDown from "@material-ui/icons/VolumeDown";
import VolumeUp from "@material-ui/icons/VolumeUp";
import debounce from "lodash/debounce";
const useStyles = makeStyles({
root: {
width: 300
}
});
const callHttpRequest = (eventSrcDesc, newValue) => {
console.log({ eventSrcDesc, newValue });
};
const topLevelDebounceCallHttpRequest = debounce(callHttpRequest, 300, {
leading: false,
trailing: true
});
export default function ContinuousSlider() {
const classes = useStyles();
const [value, setValue] = React.useState(30);
const handleChangeNoDebounce = (event, newValue) => {
setValue(newValue);
callHttpRequest("volume-no-debounce", newValue);
};
const handleChangeUsingTopLevelDebounce = (event, newValue) => {
setValue(newValue);
topLevelDebounceCallHttpRequest("volume-top-level", newValue);
};
const [stateDebounceCallHttpRequest] = React.useState(() =>
debounce(callHttpRequest, 300, {
leading: false,
trailing: true
})
);
const handleChangeUsingStateDebounce = (event, newValue) => {
setValue(newValue);
stateDebounceCallHttpRequest("volume-useState", newValue);
};
return (
<div className={classes.root}>
<Typography id="continuous-slider" gutterBottom>
Volume (No Debounce)
</Typography>
<Grid container spacing={2}>
<Grid item>
<VolumeDown />
</Grid>
<Grid item xs>
<Slider
value={value}
onChange={handleChangeNoDebounce}
aria-labelledby="continuous-slider"
/>
</Grid>
<Grid item>
<VolumeUp />
</Grid>
</Grid>
<Typography id="continuous-slider-top-level-debounce" gutterBottom>
Volume (Debounce called at top-level)
</Typography>
<Grid container spacing={2}>
<Grid item>
<VolumeDown />
</Grid>
<Grid item xs>
<Slider
value={value}
onChange={handleChangeUsingTopLevelDebounce}
aria-labelledby="continuous-slider-top-level-debounce"
/>
</Grid>
<Grid item>
<VolumeUp />
</Grid>
</Grid>
<Typography id="continuous-slider-useState-debounce" gutterBottom>
Volume (Debounce called in useState lazy initializer)
</Typography>
<Grid container spacing={2}>
<Grid item>
<VolumeDown />
</Grid>
<Grid item xs>
<Slider
value={value}
onChange={handleChangeUsingStateDebounce}
aria-labelledby="continuous-slider-useState-debounce"
/>
</Grid>
<Grid item>
<VolumeUp />
</Grid>
</Grid>
</div>
);
}
Here's a second sandbox that helps demonstrate the difference between the top-level debounce vs. the element-specific debounce: https://codesandbox.io/s/debounce-slider-onchange-jimls?file=/demo.js. In this sandbox, the entire ponent is rendered twice and the delay is 3 seconds instead of 300 milliseconds. This provides plenty of time to move the two different sliders using the top-level debounce before the end of the delay and see that only the second produces a console log; whereas doing the same thing with the two sliders using the element-specific debounced function (managed via useState) produces one console log from each element.
Lodash debounce documentation: https://lodash./docs/4.17.15#debounce
As mentioned in the ment. This is a perfect job for a debounce function. There are plenty of variations out there. Here's an example using lodash/debounce function onChange={debounce(handleChange)}
, it returns a function, which is good because the button is expecting a function reference. So on render your handleChange
is called as the debounce function parameter and then you invoke it on the actual change event, and no matter how many handleChange
events you will provide - only the last one will run.
Here's one of the implementations of a debounce function under the hood:
function debounce(func, wait, immediate) {
var timeout;
return function() {
var context = this, args = arguments;
var later = function() {
timeout = null;
if (!immediate) func.apply(context, args);
};
var callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) func.apply(context, args);
};
};
taken from here if you want to read a bit more about how it works.
UPDATE
The debounce
function, returns a function, which you need to invoke.
const boostedLabel = "foo"
const handleChange = (_event, newValue) => {
setValue(newValue);
updateWeightValue(boostedLabel, newValue)
}
and still wrap with debounce
when passing to the onChange
prop, it will have all the arguments that are being passed to the handleChange
<Slider
onChange={debounce(handleChange)}
valueLabelFormat={valueLabelFormat}
// aria-labelledby="discrete-slider"
aria-labelledby="non-linear-slider"
valueLabelDisplay="auto"
value={value}
min={1}
max={5}
step={0.05}
/>