Debouncing (fire function only once in few second) with react hooks - reactjs

I need to implement tag search on user input, but input is fast and i don't want to fire DB call for every symbol user typed, so i was curios, is there a simple a good way to debounce api calls let's say - one time after 3 seconds delay?
For now i come up with this:
let searchDelay
async function handleTagSearch(e) {
clearTimeout(searchDelay)
tagSearchPhraseSet(e.target.value)
searchDelay = setTimeout(async () => {
if (e.target.value.length > 3) {
let res = await fetch('api/tag_seatch/' + e.target.value)
res = await res.json()
console.log(res)
}
}, 3000)
}
But is it a right approach?

Your solution looks promising, if you make sure that the searchDelay number is persisted between renders with e.g. a useRef hook.
Another way of going about it is to use a useEffect hook that is run every time the input value changes. From the function given to useEffect you can return a function that clears the timeout of the previous time it was run.
Example
const { useState, useEffect } = React;
function App() {
const [value, setValue] = useState("");
const [result, setResult] = useState(null);
useEffect(
() => {
if (value.length < 3) {
setResult(null);
return;
}
const timeout = setTimeout(() => {
setResult(Math.random());
}, 3000);
return () => clearTimeout(timeout);
},
[value]
);
return (
<div>
<input value={value} onChange={e => setValue(e.target.value)} />
<div>{result}</div>
</div>
);
}
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://unpkg.com/react#16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16/umd/react-dom.development.js"></script>
<div id="root"></div>

Thanks to #Tholle example i understood that qoute "The function returned from the useEffect function will be invoked every time it is run again, and on unmount as you say" and came up with this solution:
import React, { useState, useContext, useEffect, useRef } from 'react'
export function TagsAdd() {
const [searchTerm, searchTermSet] = useState('')
const isFirstRun = useRef(true)
useEffect(() => {
//skip first run on component mount
if (isFirstRun.current) {
isFirstRun.current = false
return
}
const timeout = setTimeout(() => {
tagSearch(searchTerm)
}, 2000) //2000 - timeout to execute this function if timeout will be not cleared
return () => clearTimeout(timeout) //clear timeout (delete function execution)
}, [searchTerm])
// API call only one time in 2 seconds for the last value! Yeeeee
async function tagSearch(value) {
let res = await fetch('api/tag_seatch/' + value)
res = await res.json()
console.log(res)
}
//handle input change
function handleInput(e) {
searchTermSet(e.target.value)
}
return (
<div>
<input value={searchTerm} onChange={handleInput} />
</div>
)
}

the first thing you need to consider is using useCallback for memoization, if you just write a plain function it will be re-instantiated on each re-render. IMO you should use lodash's debounce function instead of implementing your own. the result looks something like this:
const searchTags = useCallback(debounce(async evt => {
const { value } = evt.target;
if(value.length > 3){
const response = await fetch('/api/tag_search', value);
const result = await response.json();
setTags(result) //or somewhere in your state
}
}, 3000, { trailing: true, leading: false }));

Related

Data has stopped returning after I added a debounce

I'm making an API call onChange based off the user input. I added a debounce in so it just make the request for every word. I'm doing this in a react final form FormSpy as this has implications for other fields also.
import { FormSpy } from 'react-final-form';
import { OnChange } from 'react-final-form-listeners';
import _ from 'lodash';
function Component({
handleFunction,
input,
}: {
handleFunction: any;
input: any;
}) {
const debouncedSave = useCallback(
_.debounce(async (newValue) => await getSuggestedSections(newValue), 1000),
[],
);
return (
<FormSpy subscription={{ dirty: true, values: true }}>
{() => (
<OnChange name="item-name">
{async (value) => {
const data = await debouncedSave(value);
console.log(value);
console.log(data);
}}
</OnChange>
)}
</FormSpy>
);
}
When I console log the data, it's coming back undefined, even though I can see the data in the network request.
Any ideas
You cannot await a useCallback function, to get around this issue I would handle the resolution of the promise inside the useCallback and use a use effect that has a dependency of the state that gets updated inside the useCallback.
See below for a runnable example. Ignore the script error, I've included babel runtime via a script tag to allow async functions in code snippets.
async function getSuggestedSections(value) {
return new Promise((resolve) => setTimeout(resolve(value), 2000));
}
function ExampleComponent() {
const [value, setValue] = React.useState("");
const [data, setData] = React.useState(null);
const debounceSave = React.useCallback(
_.debounce(async (newValue) => {
const data = await getSuggestedSections(newValue);
setData(data);
return data;
}, 1000),
[]
);
const handleOnChange = async (evt) => {
const { value } = evt.target;
setValue(value);
await debounceSave(value);
};
React.useEffect(() => {
if (data) {
console.log(`returned: ${data}`);
// do stuff with data
}
}, [data]);
return (
<div>
<input type="text" value={value} onChange={handleOnChange} />
</div>
);
}
ReactDOM.render(<ExampleComponent />, document.getElementById('root'));
<script src="https://unpkg.com/#babel/standalone#7/babel.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.0/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.21/lodash.min.js" integrity="sha512-WFN04846sdKMIP5LKNphMaWzU7YpMyCU245etK3g/2ARYbPK9Ub18eG+ljU96qKRCWh+quCY7yefSmlkQw1ANQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<div id="root"></div>

How does react useEffect work with useState hook?

Can someone explain what am I'm doing wrong?
I have a react functional component, where I use useEffect hook to fetch some data from server and put that data to state value. Right after fetching data, at the same useHook I need to use that state value, but the value is clear for some reason. Take a look at my example, console has an empty string, but on the browser I can see that value.
import "./styles.css";
import React, { useEffect, useState } from "react";
const App = () => {
const [value, setValue] = useState("");
function fetchHello() {
return new Promise((resolve) => {
setTimeout(() => {
resolve("Hello World");
}, 1000);
});
}
const handleSetValue = async () => {
const hello = await fetchHello();
setValue(hello);
};
useEffect(() => {
const fetchData = async () => {
await handleSetValue();
console.log(value);
};
fetchData();
}, [value]);
return (
<div className="App">
<h1>{value}</h1>
</div>
);
};
export default App;
Link to codesandbox.
The useEffect hook will run after your component renders, and it will be re-run whenever one of the dependencies passed in the second argument's array changes.
In your effect, you are doing console.log(value) but in the dependency array you didn't pass value as a dependency. Thus, the effect only runs on mount (when value is still "") and never again.
By adding value to the dependency array, the effect will run on mount but also whenever value changes (which in a normal scenario you usually don't want to do, but that depends)
import "./styles.css";
import React, { useEffect, useState } from "react";
const App = () => {
const [value, setValue] = useState("");
function fetchHello() {
return new Promise((resolve) => {
setTimeout(() => {
resolve("Hello World");
}, 1000);
});
}
const handleSetValue = async () => {
const hello = await fetchHello();
setValue(hello);
};
useEffect(() => {
const fetchData = async () => {
await handleSetValue();
console.log(value);
};
fetchData();
}, [value]);
return (
<div className="App">
<h1>{value}</h1>
</div>
);
};
export default App;
Not sure exactly what you need to do, but if you need to do something with the returned value from your endpoint you should either do it with the endpoint returned value (instead of the one in the state) or handle the state value outside the hook
import "./styles.css";
import React, { useEffect, useState } from "react";
const App = () => {
const [value, setValue] = useState("");
function fetchHello() {
return new Promise((resolve) => {
setTimeout(() => {
resolve("Hello World");
}, 1000);
});
}
const handleSetValue = async () => {
const hello = await fetchHello();
// handle the returned value here
setValue(hello);
};
useEffect(() => {
const fetchData = async () => {
await handleSetValue();
};
fetchData();
}, []);
// Or handle the value stored in the state once is set
if(value) {
// do something
}
return (
<div className="App">
<h1>{value}</h1>
</div>
);
};
export default App;

Problems with debounce in useEffect

I have a form with username input and I am trying to verify if the username is in use or not in a debounce function. The issue I'm having is that my debounce doesn't seem to be working as when I type "user" my console looks like
u
us
use
user
Here is my debounce function
export function debounce(func, wait, immediate) {
var timeout;
return () => {
var context = this, args = arguments;
var later = () => {
timeout = null;
if (!immediate) func.apply(context, args);
};
var callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) func.apply(context, args);
};
};
And here is how I'm calling it in my React component
import React, { useEffect } from 'react'
// verify username
useEffect(() => {
if(state.username !== "") {
verify();
}
}, [state.username])
const verify = debounce(() => {
console.log(state.username)
}, 1000);
The debounce function seems to be correct? Is there a problem with how I am calling it in react?
Every time your component re-renders, a new debounced verify function is created, which means that inside useEffect you are actually calling different functions which defeats the purpose of debouncing.
It's like you were doing something like this:
const debounced1 = debounce(() => { console.log(state.username) }, 1000);
debounced1();
const debounced2 = debounce(() => { console.log(state.username) }, 1000);
debounced2();
const debounced3 = debounce(() => { console.log(state.username) }, 1000);
debounced3();
as opposed to what you really want:
const debounced = debounce(() => { console.log(state.username) }, 1000);
debounced();
debounced();
debounced();
One way to solve this is to use useCallback which will always return the same callback (when you pass in an empty array as a second argument). Also, I would pass the username to this function instead of accessing the state inside (otherwise you will be accessing a stale state):
import { useCallback } from "react";
const App => () {
const [username, setUsername] = useState("");
useEffect(() => {
if (username !== "") {
verify(username);
}
}, [username]);
const verify = useCallback(
debounce(name => {
console.log(name);
}, 200),
[]
);
return <input onChange={e => setUsername(e.target.value)} />;
}
Also you need to slightly update your debounce function since it's not passing arguments correctly to the debounced function.
function debounce(func, wait, immediate) {
var timeout;
return (...args) => { <--- needs to use this `args` instead of the ones belonging to the enclosing scope
var context = this;
...
demo
Note: You will see an ESLint warning about how useCallback expects an inline function, you can get around this by using useMemo knowing that useCallback(fn, deps) is equivalent to useMemo(() => fn, deps):
const verify = useMemo(
() => debounce(name => {
console.log(name);
}, 200),
[]
);
I suggest a few changes.
1) Every time you make a state change, you trigger a render. Every render has its own props and effects. So your useEffect is generating a new debounce function every time you update username. This is a good case for useCallback hooks to keep the function instance the same between renders, or possibly useRef maybe - I stick with useCallback myself.
2) I would separate out individual handlers instead of using useEffect to trigger your debounce - you end up with having a long list of dependencies as your component grows and it's not the best place for this.
3) Your debounce function doesn't deal with params. (I replaced with lodash.debouce, but you can debug your implementation)
4) I think you still want to update the state on keypress, but only run your denounced function every x secs
Example:
import React, { useState, useCallback } from "react";
import "./styles.css";
import debounce from "lodash.debounce";
export default function App() {
const [username, setUsername] = useState('');
const verify = useCallback(
debounce(username => {
console.log(`processing ${username}`);
}, 1000),
[]
);
const handleUsernameChange = event => {
setUsername(event.target.value);
verify(event.target.value);
};
return (
<div className="App">
<h1>Debounce</h1>
<input type="text" value={username} onChange={handleUsernameChange} />
</div>
);
}
DEMO
I highly recommend reading this great post on useEffect and hooks.
export function useLazyEffect(effect: EffectCallback, deps: DependencyList = [], wait = 300) {
const cleanUp = useRef<void | (() => void)>();
const effectRef = useRef<EffectCallback>();
const updatedEffect = useCallback(effect, deps);
effectRef.current = updatedEffect;
const lazyEffect = useCallback(
_.debounce(() => {
cleanUp.current = effectRef.current?.();
}, wait),
[],
);
useEffect(lazyEffect, deps);
useEffect(() => {
return () => {
cleanUp.current instanceof Function ? cleanUp.current() : undefined;
};
}, []);
}
A simple debounce functionality with useEffect,useState hooks
import {useState, useEffect} from 'react';
export default function DebounceInput(props) {
const [timeoutId, setTimeoutId] = useState();
useEffect(() => {
return () => {
clearTimeout(timeoutId);
};
}, [timeoutId]);
function inputHandler(...args) {
setTimeoutId(
setTimeout(() => {
getInputText(...args);
}, 250)
);
}
function getInputText(e) {
console.log(e.target.value || "Hello World!!!");
}
return (
<>
<input type="text" onKeyDown={inputHandler} />
</>
);
}
I hope this works well. And below I attached vanilla js code for debounce
function debounce(cb, delay) {
let timerId;
return (...args) => {
if (timerId) clearTimeout(timerId);
timerId = setTimeout(() => {
cb(...args);
}, delay);
};
}
function getInputText(e){
console.log(e.target.value);
}
const input = document.querySelector('input');
input.addEventListener('keydown',debounce(getInputText,500));
Here's a custom hook in plain JavaScript that will achieve a debounced useEffect:
export const useDebounce = (func, timeout=100) => {
let timer;
let deferred = () => {
clearTimeout(timer);
timer = setTimeout(func, timeout);
};
const ref = useRef(deferred);
return ref.current;
};
export const useDebouncedEffect = (func, deps=[], timeout=100) => {
useEffect(useDebounce(func, timeout), deps);
}
For your example, you could use it like this:
useDebouncedEffect(() => {
if(state.username !== "") {
console.log(state.username);
}
}, [state.username])

Do an API call every few seconds using the Context API and React Hooks

Is it possible to set an auto-refresh interval every few seconds when using the Context API from React? The getData() function runs axios.get() on the API, but still when I try setInterval() and cleanup in the return function of the useEffect hook, it doesn't clean up the interval. getData() sets to the app level state the current and loading variables.
I simply want to refresh and re-do the API call every few seconds. I tried with the useRef() hook and I got it to working, but still the useEffect doesn't clear up the interval once it's finished.
I want to access the current property in the return function of the component and display some data every time an API call is ran.
Here's the code:
const { loading, current, getData } = appContext;
useEffect(() => {
const interval = setInterval(() => {
getData();
console.log('updated');
}, 1000);
return () => clearInterval(interval);
}, []); // eslint-disable-line // also tried without the []
getData() code:
const getData = async () => {
setLoading();
const res = await axios.get(process.env.REACT_APP_APIT);
dispatch({ type: GET_CURRENT, payload: res.data });
};
I had a similar problem and I used to solution described here: https://overreacted.io/making-setinterval-declarative-with-react-hooks/
Here is a simple example:
import React, { useEffect, useRef, useState } from "react";
import ReactDOM from "react-dom";
import "./styles.css";
const useInterval = (callback, delay) => {
const savedCallback = useRef();
// Remember the latest callback.
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
// Set up the interval.
useEffect(() => {
function tick() {
savedCallback.current();
}
if (delay !== null) {
let id = setInterval(tick, delay);
return () => clearInterval(id);
}
}, [delay]);
}
function App() {
const [intervalTime, setIntervalTime] = useState(2000);
useInterval(() => {
// Do some API call here
setTimeout(() => {
console.log('API call');
}, 500);
}, intervalTime);
return (
<div className="App">
<button onClick={() => setIntervalTime(2000)}>Set interval to 2 seconds</button>
<button onClick={() => setIntervalTime(null)}>Stop interval</button>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
Using the state variable intervalTime you can control the interval time. By setting it to null the interval will stop running.

How to send request on click React Hooks way?

How to send http request on button click with react hooks? Or, for that matter, how to do any side effect on button click?
What i see so far is to have something "indirect" like:
export default = () => {
const [sendRequest, setSendRequest] = useState(false);
useEffect(() => {
if(sendRequest){
//send the request
setSendRequest(false);
}
},
[sendRequest]);
return (
<input type="button" disabled={sendRequest} onClick={() => setSendRequest(true)}
);
}
Is that the proper way or is there some other pattern?
export default () => {
const [isSending, setIsSending] = useState(false)
const sendRequest = useCallback(async () => {
// don't send again while we are sending
if (isSending) return
// update state
setIsSending(true)
// send the actual request
await API.sendRequest()
// once the request is sent, update state again
setIsSending(false)
}, [isSending]) // update the callback if the state changes
return (
<input type="button" disabled={isSending} onClick={sendRequest} />
)
}
this is what it would boil down to when you want to send a request on click and disabling the button while it is sending
update:
#tkd_aj pointed out that this might give a warning: "Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function."
Effectively, what happens is that the request is still processing, while in the meantime your component unmounts. It then tries to setIsSending (a setState) on an unmounted component.
export default () => {
const [isSending, setIsSending] = useState(false)
const isMounted = useRef(true)
// set isMounted to false when we unmount the component
useEffect(() => {
return () => {
isMounted.current = false
}
}, [])
const sendRequest = useCallback(async () => {
// don't send again while we are sending
if (isSending) return
// update state
setIsSending(true)
// send the actual request
await API.sendRequest()
// once the request is sent, update state again
if (isMounted.current) // only update if we are still mounted
setIsSending(false)
}, [isSending]) // update the callback if the state changes
return (
<input type="button" disabled={isSending} onClick={sendRequest} />
)
}
You don't need an effect to send a request on button click, instead what you need is just a handler method which you can optimise using useCallback method
const App = (props) => {
//define you app state here
const fetchRequest = useCallback(() => {
// Api request here
}, [add dependent variables here]);
return (
<input type="button" disabled={sendRequest} onClick={fetchRequest}
);
}
Tracking request using variable with useEffect is not a correct pattern because you may set state to call api using useEffect, but an additional render due to some other change will cause the request to go in a loop
In functional programming, any async function should be considered as a side effect.
When dealing with side effects you need to separate the logic of starting the side effect and the logic of the result of that side effect (similar to redux saga).
Basically, the button responsibility is only triggering the side effect, and the side effect responsibility is to update the dom.
Also since react is dealing with components you need to make sure your component still mounted before any setState or after every await this depends on your own preferences.
to solve this issue we can create a custom hook useIsMounted this hook will make it easy for us to check if the component is still mounted
/**
* check if the component still mounted
*/
export const useIsMounted = () => {
const mountedRef = useRef(false);
const isMounted = useCallback(() => mountedRef.current, []);
useEffect(() => {
mountedRef.current = true;
return () => {
mountedRef.current = false;
};
});
return isMounted;
};
Then your code should look like this
export const MyComponent = ()=> {
const isMounted = useIsMounted();
const [isDoMyAsyncThing, setIsDoMyAsyncThing] = useState(false);
// do my async thing
const doMyAsyncThing = useCallback(async () => {
// do my stuff
},[])
/**
* do my async thing effect
*/
useEffect(() => {
if (isDoMyAsyncThing) {
const effect = async () => {
await doMyAsyncThing();
if (!isMounted()) return;
setIsDoMyAsyncThing(false);
};
effect();
}
}, [isDoMyAsyncThing, isMounted, doMyAsyncThing]);
return (
<div>
<button disabled={isDoMyAsyncThing} onClick={()=> setIsDoMyAsyncThing(true)}>
Do My Thing {isDoMyAsyncThing && "Loading..."}
</button>;
</div>
)
}
Note: It's always better to separate the logic of your side effect from the logic that triggers the effect (the useEffect)
UPDATE:
Instead of all the above complexity just use useAsync and useAsyncFn from the react-use library, It's much cleaner and straightforward.
Example:
import {useAsyncFn} from 'react-use';
const Demo = ({url}) => {
const [state, doFetch] = useAsyncFn(async () => {
const response = await fetch(url);
const result = await response.text();
return result
}, [url]);
return (
<div>
{state.loading
? <div>Loading...</div>
: state.error
? <div>Error: {state.error.message}</div>
: <div>Value: {state.value}</div>
}
<button onClick={() => doFetch()}>Start loading</button>
</div>
);
};
You can fetch data as an effect of some state changing like you have done in your question, but you can also get the data directly in the click handler like you are used to in a class component.
Example
const { useState } = React;
function getData() {
return new Promise(resolve => setTimeout(() => resolve(Math.random()), 1000))
}
function App() {
const [data, setData] = useState(0)
function onClick() {
getData().then(setData)
}
return (
<div>
<button onClick={onClick}>Get data</button>
<div>{data}</div>
</div>
);
}
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://unpkg.com/react#16/umd/react.development.js" crossorigin></script>
<script src="https://unpkg.com/react-dom#16/umd/react-dom.development.js" crossorigin></script>
<div id="root"></div>
You can define the boolean in the state as you did and once you trigger the request set it to true and when you receive the response set it back to false:
const [requestSent, setRequestSent] = useState(false);
const sendRequest = () => {
setRequestSent(true);
fetch().then(() => setRequestSent(false));
};
Working example
You can create a custom hook useApi and return a function execute which when called will invoke the api (typically through some onClick).
useApi hook:
export type ApiMethod = "GET" | "POST";
export type ApiState = "idle" | "loading" | "done";
const fetcher = async (
url: string,
method: ApiMethod,
payload?: string
): Promise<any> => {
const requestHeaders = new Headers();
requestHeaders.set("Content-Type", "application/json");
console.log("fetching data...");
const res = await fetch(url, {
body: payload ? JSON.stringify(payload) : undefined,
headers: requestHeaders,
method,
});
const resobj = await res.json();
return resobj;
};
export function useApi(
url: string,
method: ApiMethod,
payload?: any
): {
apiState: ApiState;
data: unknown;
execute: () => void;
} {
const [apiState, setApiState] = useState<ApiState>("idle");
const [data, setData] = useState<unknown>(null);
const [toCallApi, setApiExecution] = useState(false);
const execute = () => {
console.log("executing now");
setApiExecution(true);
};
const fetchApi = useCallback(() => {
console.log("fetchApi called");
fetcher(url, method, payload)
.then((res) => {
const data = res.data;
setData({ ...data });
return;
})
.catch((e: Error) => {
setData(null);
console.log(e.message);
})
.finally(() => {
setApiState("done");
});
}, [method, payload, url]);
// call api
useEffect(() => {
if (toCallApi && apiState === "idle") {
console.log("calling api");
setApiState("loading");
fetchApi();
}
}, [apiState, fetchApi, toCallApi]);
return {
apiState,
data,
execute,
};
}
using useApi in some component:
const SomeComponent = () =>{
const { apiState, data, execute } = useApi(
"api/url",
"POST",
{
foo: "bar",
}
);
}
if (apiState == "done") {
console.log("execution complete",data);
}
return (
<button
onClick={() => {
execute();
}}>
Click me
</button>
);
For this you can use callback hook in ReactJS and it is the best option for this purpose as useEffect is not a correct pattern because may be you set state to make an api call using useEffect, but an additional render due to some other change will cause the request to go in a loop.
<const Component= (props) => {
//define you app state here
const getRequest = useCallback(() => {
// Api request here
}, [dependency]);
return (
<input type="button" disabled={sendRequest} onClick={getRequest}
);
}
My answer is simple, while using the useState hook the javascript doesn't enable you to pass the value if you set the state as false. It accepts the value when it is set to true. So you have to define a function with if condition if you use false in the usestate

Resources