ReactJS delay onChange while typing - javascript

I need the state to change to maintain the string the user is typing. However I want to delay an action until the user stops typing. But I can't quite place my finger on how to do both.
So When the user stops typing I want an action to be triggered, but not before. Any suggestions?

With React Hooks and Function components
To keep the string the user is typing, use the useState hook to store the text the user is typing. Then give that state to the value of the input. Also be sure to use setState on the onChange event handler of the input, otherwise the input value won't change.
To trigger an action only sometime after the user stops typing, you can use the useEffect hook together with setTimeout. In this case, we want to trigger useEffect when the input value changes, so we'll create a useEffect hook and on its dependency array give it the variable with the value of the input. The function given to useEffect should use setTimeout to trigger an action after the delay time that is desired. Also, the function given to useEffect should return a cleanup function that clears the timeout set. This avoids doing actions for input values which are no longer relevant to the user.
Below is a little example of an app that uses the above steps to keep the string the user is typing visible and to show the finished string 500ms after the user stops typing.
function App() {
const [query, setQuery] = useState("");
const [displayMessage, setDisplayMessage] = useState("");
useEffect(() => {
const timeOutId = setTimeout(() => setDisplayMessage(query), 500);
return () => clearTimeout(timeOutId);
}, [query]);
return (
<>
<input
type="text"
value={query}
onChange={event => setQuery(event.target.value)}
/>
<p>{displayMessage}</p>
</>
);
}

Sounds you are going to need to use setTimeout to start a timer as soon as the user enters text. If the user enters another character, restart the timer. If the user does not type again before the timer completes, it will fire an action that toggles the checkbox:
class App extends React.Component {
constructor() {
super();
this.state = {
text: '',
checked: false
};
this.timer = null;
}
componentDidUpdate (prevProps, prevState) {
if(prevState.text !== this.state.text) {
this.handleCheck();
}
}
onChange = e => {
this.setState({
text: e.target.value
});
};
handleCheck = () => {
// Clears running timer and starts a new one each time the user types
clearTimeout(this.timer);
this.timer = setTimeout(() => {
this.toggleCheck();
}, 1000);
}
toggleCheck = () => {
this.setState( prevState => ({ checked: !prevState.checked }));
}
render () {
return (
<div>
<input value={this.state.text} onChange={this.onChange} placeholder="Start typing..." /><br/>
<label>
<input type="checkbox" checked={this.state.checked} onChange={this.toggleCheck} />
Toggle checkbox after user stops typing for 1 second
</label>
</div>
)
}
}
ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="root"></div>

One way to do this would be to have your onChange handler execute two functions:
Function for immediately updating state
Debounced function
Example code:
import debounce from 'lodash.debounce';
class Foo extends React.Component {
constructor() {
super()
this.state = {
value: ''
}
// Delay action 2 seconds
this.onChangeDebounced = debounce(this.onChangeDebounced, 2000)
}
handleInputChange = (e: Event) => {
// Immediately update the state
this.setState({
value: e.target.value
})
// Execute the debounced onChange method
this.onChangeDebounced(e)
}
onChangeDebounced = (e: Event) => {
// Delayed logic goes here
}
render() {
return (
<input onChange={this.handleInputChange} value={this.state.value} />
)
}
}

With React Hooks and Function components
const [timer, setTimer] = useState(null);
function changeDelay(change) {
if (timer) {
clearTimeout(timer);
setTimer(null);
}
setTimer(
setTimeout(() => {
console.log(change);
}, 3000)
);
}
In input
<input type="text" onChange={(e) => { changeDelay(e.target.value); }} />

With React Hooks - useRef
const timer = useRef(null)
useEffect(() => {
clearTimeout(timer.current)
timer.current = setTimeout(() => {
// your logic
},1000)
},[value])

Call every state update except the first time:
Actually, I have the same issue but a little setTimeout could help me with a check ref for the first time mount:
import React, {useState, useEffect, useRef} from "react";
const Search = () => {
const filterRef = useRef(); // use ref to call the API call all time except first time
const [serpQuery, setSerpQuery] = useState('');
useEffect(() => {
let delayTimeOutFunction;
if(!filterRef.current) {
filterRef.current = true;
} else { // componentDidMount equivalent
delayTimeOutFunction = setTimeout(() => {
console.log('call api: ', serpQuery)
}, 700); // denounce delay
}
return () => clearTimeout(delayTimeOutFunction);
}, [serpQuery]);
return (
<input value={serpQuery} onChange={e => setSerpQuery(e.target.value)} />
);
};

You can build a custom hook specifically for this purpose and use it just like the useState hook. This is more like an extension of jnforja's answer
import { useEffect, useState } from "react";
const useDebounce = (initialValue = "", delay) => {
const [actualValue, setActualValue] = useState(initialValue);
const [debounceValue, setDebounceValue] = useState(initialValue);
useEffect(() => {
const debounceId = setTimeout(() => setDebounceValue(actualValue), delay);
return () => clearTimeout(debounceId);
}, [actualValue, delay]);
return [debounceValue, setActualValue];
};
export default useDebounce;
And use it just like the useState hook with the delay value
const [value, setValue] = useDebounce('',1000)
You can also check this article, explaining the implementation if you want.

You can debounce on the onChange event (if the user is typing the onchange event will not execute)
Warning - Keep in mind that creating functions on render is a bad practice.
I did it in order to illustrate the solution.
A more safe solution is to use a class Component that creates the debounced handler on its constructor.
class DebouncedInput extends React.Component {
constructor() {
super();
// Creating the debouncedOnChange to avoid performance issues
this._debouncedOnChange = _.debounce(
this.props.onChange,
this.props.delay
);
}
render () {
const { onChange, delay, ...rest } = this.props;
return (
<input onChange={this._debouncedOnChange} {..rest} />
)
}
}
Example below
function DebouncedInput (props) {
const { onChange, delay = 300, ...rest } = props;
return (
<input
{...rest}
onChange={ _.debounce(onChange, delay)}
/>
)
}
function App() {
return (
<div>
<DebouncedInput
type="text"
placeholder="enter"
delay={2000}
onChange={() => console.log('changing')}
/>
</div>
)
}
ReactDOM.render(
<App/>,
document.querySelector('#app')
);
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>JS Bin</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.4.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.4.2/umd/react-dom.production.min.js"></script>
</head>
<body>
<div id="app"></div>
</body>
</html>

You can use debounce and throttle of lodash library for delaying to call change handler function, the following code is based on debounce. The same code can be used for the throttle function.
Debounce: delays invoking function until after X milliseconds
Throttle: invokes function at most once per every X milliseconds
Sample code:
import React,{useEffect, useState, useMemo} from "react"
import debounce from "lodash.debounce";
export default function App() {
const [search, setSearch] = useState("");
const handleChangeSearch = ({ target }) => {
setSearch(target.value);
};
const debouncedChangeHandler = useMemo(
() => debounce(handleChangeSearch, 500),
[]
);
useEffect(() => {
return () => {
debouncedChangeHandler.cancel();
}
}, []);
return (
<div className="App">
<label > Search:
<input sx={{ display: { xs: "none", md: "block" } }}
onChange={debouncedChangeHandler}
name="search"
type="text"
placeholder="search..."
/>
</label >
</div>
);
}

I have created npm package for this matter, you can use provided hook to get both immediate and delayed values.
https://www.npmjs.com/package/use-delayed-search

Related

Send a http request to the server with a delay due to content changes [duplicate]

I have a here a input field that on every type, it dispatches a redux action.
I have put a useDebounce in order that it won't be very heavy. The problem is that it says Hooks can only be called inside of the body of a function component. What is the proper way to do it?
useTimeout
import { useCallback, useEffect, useRef } from "react";
export default function useTimeout(callback, delay) {
const callbackRef = useRef(callback);
const timeoutRef = useRef();
useEffect(() => {
callbackRef.current = callback;
}, [callback]);
const set = useCallback(() => {
timeoutRef.current = setTimeout(() => callbackRef.current(), delay);
}, [delay]);
const clear = useCallback(() => {
timeoutRef.current && clearTimeout(timeoutRef.current);
}, []);
useEffect(() => {
set();
return clear;
}, [delay, set, clear]);
const reset = useCallback(() => {
clear();
set();
}, [clear, set]);
return { reset, clear };
}
useDebounce
import { useEffect } from "react";
import useTimeout from "./useTimeout";
export default function useDebounce(callback, delay, dependencies) {
const { reset, clear } = useTimeout(callback, delay);
useEffect(reset, [...dependencies, reset]);
useEffect(clear, []);
}
Form component
import React from "react";
import TextField from "#mui/material/TextField";
import useDebounce from "../hooks/useDebounce";
export default function ProductInputs(props) {
const { handleChangeProductName = () => {} } = props;
return (
<TextField
fullWidth
label="Name"
variant="outlined"
size="small"
name="productName"
value={formik.values.productName}
helperText={formik.touched.productName ? formik.errors.productName : ""}
error={formik.touched.productName && Boolean(formik.errors.productName)}
onChange={(e) => {
formik.setFieldValue("productName", e.target.value);
useDebounce(() => handleChangeProductName(e.target.value), 1000, [
e.target.value,
]);
}}
/>
);
}
I don't think React hooks are a good fit for a throttle or debounce function. From what I understand of your question you effectively want to debounce the handleChangeProductName function.
Here's a simple higher order function you can use to decorate a callback function with to debounce it. If the returned function is invoked again before the timeout expires then the timeout is cleared and reinstantiated. Only when the timeout expires is the decorated function then invoked and passed the arguments.
const debounce = (fn, delay) => {
let timerId;
return (...args) => {
clearTimeout(timerId);
timerId = setTimeout(() => fn(...args), delay);
}
};
Example usage:
export default function ProductInputs({ handleChangeProductName }) {
const debouncedHandler = useCallback(
debounce(handleChangeProductName, 200),
[handleChangeProductName]
);
return (
<TextField
fullWidth
label="Name"
variant="outlined"
size="small"
name="productName"
value={formik.values.productName}
helperText={formik.touched.productName ? formik.errors.productName : ""}
error={formik.touched.productName && Boolean(formik.errors.productName)}
onChange={(e) => {
formik.setFieldValue("productName", e.target.value);
debouncedHandler(e.target.value);
}}
/>
);
}
If possible the parent component passing the handleChangeProductName callback as a prop should probably handle creating a debounced, memoized handler, but the above should work as well.
Taking a look at your implementation of useDebounce, and it doesn't look very useful as a hook. It seems to have taken over the job of calling your function, and doesn't return anything, but most of it's implementation is being done in useTimeout, which also not doing much...
In my opinion, useDebounce should return a "debounced" version of callback
Here is my take on useDebounce:
export default function useDebounce(callback, delay) {
const [debounceReady, setDebounceReady] = useState(true);
const debouncedCallback = useCallback((...args) => {
if (debounceReady) {
callback(...args);
setDebounceReady(false);
}
}, [debounceReady, callback]);
useEffect(() => {
if (debounceReady) {
return undefined;
}
const interval = setTimeout(() => setDebounceReady(true), delay);
return () => clearTimeout(interval);
}, [debounceReady, delay]);
return debouncedCallback;
}
Usage will look something like:
import React from "react";
import TextField from "#mui/material/TextField";
import useDebounce from "../hooks/useDebounce";
export default function ProductInputs(props) {
const handleChangeProductName = useCallback((value) => {
if (props.handleChangeProductName) {
props.handleChangeProductName(value);
} else {
// do something else...
};
}, [props.handleChangeProductName]);
const debouncedHandleChangeProductName = useDebounce(handleChangeProductName, 1000);
return (
<TextField
fullWidth
label="Name"
variant="outlined"
size="small"
name="productName"
value={formik.values.productName}
helperText={formik.touched.productName ? formik.errors.productName : ""}
error={formik.touched.productName && Boolean(formik.errors.productName)}
onChange={(e) => {
formik.setFieldValue("productName", e.target.value);
debouncedHandleChangeProductName(e.target.value);
}}
/>
);
}
Debouncing onChange itself has caveats. Say, it must be uncontrolled component, since debouncing onChange on controlled component would cause annoying lags on typing.
Another pitfall, we might need to do something immediately and to do something else after a delay. Say, immediately display loading indicator instead of (obsolete) search results after any change, but send actual request only after user stops typing.
With all this in mind, instead of debouncing callback I propose to debounce sync-up through useEffect:
const [text, setText] = useState('');
const isValueSettled = useIsSettled(text);
useEffect(() => {
if (isValueSettled) {
props.onChange(text);
}
}, [text, isValueSettled]);
...
<input value={value} onChange={({ target: { value } }) => setText(value)}
And useIsSetlled itself will debounce:
function useIsSettled(value, delay = 500) {
const [isSettled, setIsSettled] = useState(true);
const isFirstRun = useRef(true);
const prevValueRef = useRef(value);
useEffect(() => {
if (isFirstRun.current) {
isFirstRun.current = false;
return;
}
setIsSettled(false);
prevValueRef.current = value;
const timerId = setTimeout(() => {
setIsSettled(true);
}, delay);
return () => { clearTimeout(timerId); }
}, [delay, value]);
if (isFirstRun.current) {
return true;
}
return isSettled && prevValueRef.current === value;
}
where isFirstRun is obviously save us from getting "oh, no, user changed something" after initial rendering(when value is changed from undefined to initial value).
And prevValueRef.current === value is not required part but makes us sure we will get useIsSettled returning false in the same render run, not in next, only after useEffect executed.

how to get current value from input field in react js

I am trying to get current value from input field, but after onclick I am getting preious value in colsole.
here is my code
import { React, useState } from "react";
const CompoundIntrest = () => {
const [capitalValue, setcapitalValue] = useState(1000);
const ChangeCapital = () => {
setcapitalValue(capitalValue - 100);
};
const Calculate = () => {
console.log(capitalValue);
};
return (
<>
<button
onClick={() => {
ChangeCapital();
Calculate();
}}
>
click
</button>
<input type="number" value={capitalValue} />
</>
);
};
export default CompoundIntrest;
State updates occur asynchronously, so you won't have the updated state value inside the event handler.
You can lift the new value i.e. capitalValue - 100 to a scope from where it can be passed down to both ChangeCapital & Calculate.
const CompoundIntrest = () => {
const [capitalValue, setCapitalValue] = React.useState(1000);
const handleClick = () => {
const newCapitalValue = capitalValue - 100;
ChangeCapital(newCapitalValue);
Calculate(newCapitalValue);
};
const ChangeCapital = (capitalValue) => {
setCapitalValue(capitalValue);
};
const Calculate = (capitalValue) => {
console.log(capitalValue);
};
return (
<React.Fragment>
<button onClick={handleClick}>click</button>
<input
type="number"
value={capitalValue}
onChange={(e) => setCapitalValue(e.target.value)}
/>
</React.Fragment>
);
};
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<CompoundIntrest />);
<script crossorigin src="https://unpkg.com/react#18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#18/umd/react-dom.development.js"></script>
<div id="root"></div>
Note: The state updater function is called synchronously but the state updates happen asynchronously.
This becomes more clear if you update the state by passing a state updater callback, you would see that the callback is fired synchronously. Notice the order of logs in the example below:
function App() {
const [count, setCount] = React.useState(0);
const handleClick = () => {
console.log("Before calling setCount");
setCount((currCount) => {
console.log("Inside setCount");
return currCount + 1;
});
console.log("After calling setCount");
};
return <button onClick={handleClick}>Count: {count}</button>;
}
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<App />);
<script crossorigin src="https://unpkg.com/react#18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#18/umd/react-dom.development.js"></script>
<div id="root"></div>
You can use Use useEffect Like this:-
import React,{useState,useEffect} from "react";
const CompoundIntrest = () => {
const [capitalValue, setcapitalValue] = useState(1000);
const ChangeCapital = () => {
setcapitalValue(capitalValue - 100);
};
const Calculate = () => {
console.log(capitalValue);
};
useEffect(()=>{
console.log("afet chage",capitalValue);
},[capitalValue]);
return (
<>
<button
onClick={() => {
ChangeCapital();
Calculate();
}}
>
click
</button>
<input type="number" value={capitalValue} />
</>
);
};
You can use the onChange event in the input field to get current value.
const [currentValue, setCurrentValue] = useState['']
const changeHandler = (e:any) => {
e.preventDefault();
const { value } = e.target
console.log('value', value);
setCurrentValue(value)
}
<input type="string" value={currentValue} onChange={(e:any) => changeHandler(e)}/>
I think, you should add onChange method in input tag like below:
Then you get current value in onClick event in button tag.
import { React, useState } from "react";
const CompoundIntrest = () => {
const [capitalValue, setcapitalValue] = useState(1000);
const ChangeCapital = () => {
setcapitalValue(capitalValue - 100);
};
useEffect(() => {
const Calculate = () => {
console.log(capitalValue);
};
Calculate()
}, [capitalValue])
return (
<>
<button
onClick={() => {
ChangeCapital();
}}
>
click
</button>
<input type="number" value={capitalValue} onChange={(e) => setcapitalValue(e.target.value)} />
</>
);
};
export default CompoundIntrest;
In the case of controlled component, other members have already provided the answer, I just want to give you an idea about uncontrolled component.
Assuming that we are dealing with an uncontrolled component ( "input" element ) then how we can get the value.
1. import { React, useState, useRef, useEffect } from "react";
2.
3. const CompoundIntrest = () => {
4. const [capitalValue, setcapitalValue] = useState(1000);
5. const inputRef = useRef(null);
6.
7. useEffect(() => {
8. console.log(capitalValue);
9. }, [capitalValue]);
10.
11. const ChangeCapital = () => {
12. setcapitalValue(inputRef.current.value - 100);
13. };
14.
15. return (
16. <>
17. <button onClick={ChangeCapital}>click</button>
18. <input ref={inputRef} type="number" />
19. </>
20. );
21. };
22.
23. export default CompoundIntrest;
At line 5, we have created a ref with initial value null using useRef hook of react, which later will be used to store reference of input element.
At line 18, we have assigned the inputRef to the ref of input element, which will be use to get the value from the field.
At line 12, we are getting the value of input as inputRef.current.value .
To check the update in the value of capitalValue state onClick event of button we can use useEffect hook of react ( From Line 7 to Line 9 is doing the same ).
PS : Please let me know if this clear your doubt or not. Thanks for reading the answer.
your code is fine, and your state is successfuly updated, the problem is the timing of calling your console. react handles your code async, it means it starts your changeCapital, and before the change capital function is finished it calls the calculate function, so the value of your state, is the previous value.
you need to call your calculate function somewhere else:
you can call it in a UseEffect hook, this way your function gets called whenever your state has successfuly changed, or
you can call your calculate in 'onchange' event of your input feild
if you want the better solution, the first one is more reactly than the second one

Why KeyboardEvent isn't working with this Input element in react?

I'm working with controlled input elements at work and I'm stuck.
Basically, I need to autofill some input elements in a form, but the problem is that I need to fill it in a way that simulates the user input (in this case, typing) in order to trigger the onChange function's logic. So, because of that. I need to emulate the typing behavior and not just set the value for the element.
Despite having searched for previous questions and reading docs about KeyboardEvent, I haven't been able to make this work.
Currently, I'm experimenting in a Codesandbox just for making things easier, but even with this simple environment, I can't manage to get this to work.
Here's the code and its Codesandbox link
import { useRef, useState, useEffect } from "react";
import "./styles.css";
export default function App() {
const [state, setState] = useState();
const inputRef = useRef();
const event = new KeyboardEvent("keypress", { key: 99 });
useEffect(() => {
inputRef.current.dispatchEvent(event);
}, [inputRef]);
const onChange = (e) => {
setState(e.target.value);
};
return (
<div className="App">
<h1>{state}</h1>
<input
type="text"
id="name"
onChange={onChange}
ref={inputRef}
value={state}
/>
</div>
);
}
Hopefully one of you guys could give me a hand with this.
Thanks for reading!
Related to the comments:
I think that it shouldn't be necessary to be dispatching a keypress event to get your special effect logic to run.
For example, you can use a useEffect which just runs on initial render to trigger whatever special logic you want -- and this way you can just have a regular initial value for the form state.
import { useState, useEffect } from "react";
import "./styles.css";
export default function App() {
// In the useState call, you can initialize the value.
const [state, setState] = useState("initial value");
const specialEffectFunction = () => {
// here's the code for the special effect you want to run on load
console.log('this is the special onChange effect')
}
useEffect(() => {
// This will trigger the special function which you want to run
// when the app loads
specialEffectFunction();
// if it really HAS to be the `onChange` function that's called,
// then you'll need to call that with a fake ChangeEvent.. but I don't
// think that should be necessary...
}, [])
const onChange = (e) => {
setState(e.target.value);
};
return (
<div className="App">
<h1>{state}</h1>
<input
type="text"
id="name"
onChange={onChange}
value={state}
/>
</div>
);
}
I couldn't fix the problem with Keyboard Event for my lack of knowledge about it, but I hope I managed to solve the problem of emulating a human autofill the input using the below code.
function AutoFillInput({ finalValue }: { finalValue: string }) {
const [inputValue, setInputValue] = useState('');
const [sliceStart, setSliceStart] = useState(0);
const changeHandler = useCallback((event) => {
setInputValue(event.target.value);
}, []);
useEffect(function handleFinalValueChange() {
setInputValue('');
if (sliceStart < finalValue.length)
setSliceStart(x => x + 1);
}, [finalValue]);
useEffect(function handleSlice() {
setInputValue(finalValue.slice(0, sliceStart));
if (sliceStart < finalValue.length) {
setTimeout(() => {
setSliceStart(x => x + 1);
}, 800);
}
}, [sliceStart]);
return (
<input
value={inputValue}
onChange={changeHandler}
placeholder={'Auto fill input'}
/>
)
}
function App() {
return (
<div >
<AutoFillInput finalValue={'hello world'} />
</div>
);
}
export default App;

Record HTML input range value only once dragging is stopped

How to call the function only when the dragging of Input Range is stopped?
state={
range:500
}
changeHandle=(e)=>{
const range=e.currentTarget.value;
this.setState({range});
this.connectToServer(range); //call this function only when dragging is stopped
}
connectToServer=async(value)=>{
await axios.............
}
HTML
<input onChange={this.handleChange} value={this.state.range} type="range" step="500" min="500" max="10000" />
You likely don't want to hammer your endpoint with all the extraneous intermediate onChange values, so you basically need to debounce the input's onChange handler.
Here's a simple debounce utility I occasionally use:
const debounce = (fn, delay) => {
let timerId;
return (...args) => {
clearTimeout(timerId);
timerId = setTimeout(() => fn(...args), delay);
};
};
But any 3rd-party package like lodash would work.
The next steps are key, you don't want to block the state updates so you'll want to split out the state update from the server call.
You want to issue the side-effect of calling your backend in the componentDidUpdate lifecycle method for two reasons.
It's the correct method for side-effects.
It ensures you have the latest this.state.range value
Code:
connectToServer = (value) => console.log("Send to server", value);
debouncedConnectToServer = debounce(this.connectToServer, 500);
componentDidUpdate(prevProps, prevState) {
if (prevState.range !== this.state.range) {
this.debouncedConnectToServer(this.state.range);
}
}
handleChange = (e) => {
const range = e.currentTarget.value;
this.setState({ range });
};
Demo
Full demo code:
const debounce = (fn, delay) => {
let timerId;
return (...args) => {
clearTimeout(timerId);
timerId = setTimeout(() => fn(...args), delay);
};
};
export default class App extends React.Component {
state = {
range: 500
};
connectToServer = (value) => console.log("Send to server", value);
debouncedConnectToServer = debounce(this.connectToServer, 500);
componentDidUpdate(prevProps, prevState) {
if (prevState.range !== this.state.range) {
this.debouncedConnectToServer(this.state.range);
}
}
handleChange = (e) => {
const range = e.currentTarget.value;
this.setState({ range });
};
render() {
return (
<label>
Input
<input
onChange={this.handleChange}
value={this.state.range}
type="range"
step="500"
min="500"
max="10000"
/>
</label>
);
}
}
I assume that you want to prevent hitting the API over and over again. Actually there is another solution for this, which is using debounce function.
TLDR; what debounce doing is delaying the function calling by such duration.
I assume that you're using reactJS, so you can achieve it like this using lodash
import debounce from 'lodash/debounce'
export default function App() {
const debounceFunc = debounce((val) => handleChange(val), 500)
const handleChange = val => {
console.log(val)
// hit API here
}
return (
<div className="App">
<input
type="range"
min="0"
max={100}
onChange={e => {
debounceFunc(e.target.value)
}}
/>
</div>
);
}
you can try it here on codesandbox.
And this is the documentation
There is also another way using debounce, by creating custom hooks useDebounce. If you're interested on it, you can check it here

Why eventListener re-render React Component

I am creating a stopwatch in React.js and i am wondering why window.addEventListener('keydown', callback) re-render my component?
import { useEffect, useState } from 'react';
import './App.scss';
import Timer from './Timer';
import Button from './Button';
import Time from './Time';
const App = () => {
const [isRunning, setIsRunning] = useState(false);
const [start, setStart] = useState(new Time(0));
const [stop, setStop] = useState(new Time(0));
const handleStart = () => {
const now = new Date();
setIsRunning(true);
setStart(new Time(now));
setStop(new Time(now));
};
const handleStop = () => {
setIsRunning(false);
setStop(new Time(new Date()));
};
const getTime = () => {
if (isRunning) {
return new Time(new Date().getTime() - start.origin);
} else {
return new Time(stop.origin - start.origin);
}
};
const handleKeyDown = (key) => {
console.log(key.code === 'Space');
};
useEffect(() => {
window.addEventListener('keydown', handleKeyDown);
return () => {
window.removeEventListener('keydown', handleKeyDown);
};
});
return (
<div className="stopwatch">
<Timer getTime={getTime} />
<div className="buttons">
<Button role={'start'} callback={handleStart} />
<Button role={'stop'} callback={handleStop} />
</div>
</div>
);
};
export default App;
When i click start and then stop after let's say 3s. <Timer /> show correctly time that has passed, but then when i press Space on keyboard <Timer /> is re-rendering, showing new time. Then, when i switch my web-browser to VSCode and again to web-browser, <Timer /> isn't re-rendering
Here is my Timer component
import { memo, useEffect, useRef } from 'react';
const Timer = ({ getTime }) => {
const timer = useRef();
console.log('timer rendered');
useEffect(() => {
function run() {
const time = getTime().formatted();
timer.current.textContent = `${time.m}:${time.s}.${time.ms}`;
requestAnimationFrame(run);
}
run();
return () => {
cancelAnimationFrame(run);
};
});
return <div ref={timer} className="timer"></div>;
};
export default memo(Timer);
no matter if I use [] in both or none of useEffect nothing changes.
As #davood-falahati says, adding an empty array as a second argument to useEffect would probably be desirable. From the docs:
... If you want to run an effect and clean it up only once (on mount and unmount), you can pass an empty array ([]) as a second argument. This tells React that your effect doesn’t depend on any values from props or state, so it never needs to re-run. This isn’t handled as a special case — it follows directly from how the dependencies array always works. ...
In your use case:
useEffect(() => {
window.addEventListener('keydown', handleKeyDown);
return () => {
window.removeEventListener('keydown', handleKeyDown);
};
}, []);

Categories

Resources