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])
Related
I created a hook to be able to set a timeout. Every callback that will be added in that hook will be delayed with a certain amount of time:
import "./styles.css";
import React, { useRef, useCallback, useState, useEffect } from "react";
const useTimer = () => {
const timer = useRef();
const fn = useCallback((callback, timeout = 0) => {
timer.current = setTimeout(() => {
callback();
}, timeout);
}, []);
const clearTimeoutHandler = () => {
console.log("clear");
return clearTimeout(timer);
};
useEffect(() => {
return clearTimeoutHandler();
}, []);
return { fn, clearTimeoutHandler };
};
export default function App() {
const [state, setState] = useState(false);
const timer = useTimer();
const onClickHandler = () => {
setState(true);
timer.fn(() => {
console.log(5000);
setState(false);
}, 5000);
};
return (
<div className="App">
{state && <h1>Hello</h1>}
<button onClick={onClickHandler}>Open</button>
</div>
);
}
In my case Hello text will appear after user will click on the button and will disappear after 5000 sec. Issue: After clicking 1 time on the button and after 3 sec again clicking on it i expect to see Hello text 5 sec right after the last click, but in my case if i click twice the timer will take into account only the first click and if i will click after 3 sec one more time the text will disappear after 2 sec not 5. Question: how to fix the hook and to reset the timer if the user click many times or the hook is called many times?
demo: https://codesandbox.io/s/recursing-ride-mlqnri?file=/src/App.js:0-880
All you need is to add clearTimeout() to your useTimer() function.
Here's your working code:
import "./styles.css";
import React, { useRef, useCallback, useState, useEffect } from "react";
const useTimer = () => {
const timer = useRef();
const fn = useCallback((callback, timeout = 0) => {
// Add this line here
clearTimeout(timer.current);
timer.current = setTimeout(() => {
callback();
}, timeout);
}, []);
const clearTimeoutHandler = () => {
console.log("clear");
return clearTimeout(timer);
};
useEffect(() => {
return clearTimeoutHandler();
}, []);
return { fn, clearTimeoutHandler };
};
export default function App() {
const [state, setState] = useState(false);
const timer = useTimer();
const onClickHandler = () => {
setState(true);
timer.fn(() => {
console.log(5000);
setState(false);
}, 5000);
};
return (
<div className="App">
{state && <h1>Hello</h1>}
<button onClick={onClickHandler}>Open</button>
</div>
);
}
And a sandbox demo: https://codesandbox.io/s/angry-dream-xmzh9v?file=/src/App.js
You are passing the ref directly to clearTimeout
const clearTimeoutHandler = () => {
console.log("clear");
return clearTimeout(timer); // should be timer.current
};
I think you should clear previous timer in fn function, and assign null to timer.current after callback function is invoke.
this is my code below:
https://codesandbox.io/s/exciting-fire-jgujww?file=/src/App.js:0-977
I'm checking if a component is unmounted, in order to avoid calling state update functions.
This is the first option, and it works
const ref = useRef(false)
useEffect(() => {
ref.current = true
return () => {
ref.current = false
}
}, [])
....
if (ref.current) {
setAnswers(answers)
setIsLoading(false)
}
....
Second option is using useState, which isMounted is always false, though I changed it to true in component did mount
const [isMounted, setIsMounted] = useState(false)
useEffect(() => {
setIsMounted(true)
return () => {
setIsMounted(false)
}
}, [])
....
if (isMounted) {
setAnswers(answers)
setIsLoading(false)
}
....
Why is the second option not working compared with the first option?
I wrote this custom hook that can check if the component is mounted or not at the current time, useful if you have a long running operation and the component may be unmounted before it finishes and updates the UI state.
import { useCallback, useEffect, useRef } from "react";
export function useIsMounted() {
const isMountedRef = useRef(true);
const isMounted = useCallback(() => isMountedRef.current, []);
useEffect(() => {
return () => void (isMountedRef.current = false);
}, []);
return isMounted;
}
Usage
function MyComponent() {
const [data, setData] = React.useState()
const isMounted = useIsMounted()
React.useEffect(() => {
fetch().then((data) => {
// at this point the component may already have been removed from the tree
// so we need to check first before updating the component state
if (isMounted()) {
setData(data)
}
})
}, [...])
return (...)
}
Live Demo
Please read this answer very carefully until the end.
It seems your component is rendering more than one time and thus the isMounted state will always become false because it doesn't run on every update. It just run once and on unmounted. So, you'll do pass the state in the second option array:
}, [isMounted])
Now, it watches the state and run the effect on every update. But why the first option works?
It's because you're using useRef and it's a synchronous unlike asynchronous useState. Read the docs about useRef again if you're unclear:
This works because useRef() creates a plain JavaScript object. The only difference between useRef() and creating a {current: ...} object yourself is that useRef will give you the same ref object on every render.
BTW, you do not need to clean up anything. Cleaning up the process is required for DOM changes, third-party api reflections, etc. But you don't need to habit on cleaning up the states. So, you can just use:
useEffect(() => {
setIsMounted(true)
}, []) // you may watch isMounted state
// if you're changing it's value from somewhere else
While you use the useRef hook, you are good to go with cleaning up process because it's related to dom changes.
This is a typescript version of #Nearhuscarl's answer.
import { useCallback, useEffect, useRef } from "react";
/**
* This hook provides a function that returns whether the component is still mounted.
* This is useful as a check before calling set state operations which will generates
* a warning when it is called when the component is unmounted.
* #returns a function
*/
export function useMounted(): () => boolean {
const mountedRef = useRef(false);
useEffect(function useMountedEffect() {
mountedRef.current = true;
return function useMountedEffectCleanup() {
mountedRef.current = false;
};
}, []);
return useCallback(function isMounted() {
return mountedRef.current;
}, [mountedRef]);
}
This is the jest test
import { render, waitFor } from '#testing-library/react';
import React, { useEffect } from 'react';
import { delay } from '../delay';
import { useMounted } from "./useMounted";
describe("useMounted", () => {
it("should work and not rerender", async () => {
const callback = jest.fn();
function MyComponent() {
const isMounted = useMounted();
useEffect(() => {
callback(isMounted())
}, [])
return (<div data-testid="test">Hello world</div>);
}
const { unmount } = render(<MyComponent />)
expect(callback.mock.calls).toEqual([[true]])
unmount();
expect(callback.mock.calls).toEqual([[true]])
})
it("should work and not rerender and unmount later", async () => {
jest.useFakeTimers('modern');
const callback = jest.fn();
function MyComponent() {
const isMounted = useMounted();
useEffect(() => {
(async () => {
await delay(10000);
callback(isMounted());
})();
}, [])
return (<div data-testid="test">Hello world</div>);
}
const { unmount } = render(<MyComponent />)
await waitFor(() => expect(callback).toBeCalledTimes(0));
jest.advanceTimersByTime(5000);
unmount();
jest.advanceTimersByTime(5000);
await waitFor(() => expect(callback).toBeCalledTimes(1));
expect(callback.mock.calls).toEqual([[false]])
})
})
Sources available in https://github.com/trajano/react-hooks-tests/tree/master/src/useMounted
This cleared up my error message, setting a return in my useEffect cancels out the subscriptions and async tasks.
import React from 'react'
const MyComponent = () => {
const [fooState, setFooState] = React.useState(null)
React.useEffect(()=> {
//Mounted
getFetch()
// Unmounted
return () => {
setFooState(false)
}
})
return (
<div>Stuff</div>
)
}
export {MyComponent as default}
If you want to use a small library for this, then react-tidy has a custom hook just for doing that called useIsMounted:
import React from 'react'
import {useIsMounted} from 'react-tidy'
function MyComponent() {
const [data, setData] = React.useState(null)
const isMounted = useIsMounted()
React.useEffect(() => {
fetchData().then((result) => {
if (isMounted) {
setData(result)
}
})
}, [])
// ...
}
Learn more about this hook
Disclaimer I am the writer of this library.
Near Huscarl solution is good, but there is problem with using these hook with react router, because if you go from example news/1 to news/2 useRef value is set to false because of unmount, but value keep false. So you need init ref value to true on each mount.
import {useRef, useCallback, useEffect} from "react";
export function useIsMounted(): () => boolean {
const isMountedRef = useRef(true);
const isMounted = useCallback(() => isMountedRef.current, []);
useEffect(() => {
isMountedRef.current = true;
return () => void (isMountedRef.current = false);
}, []);
return isMounted;
}
It's hard to know without the larger context, but I don't think you even need to know whether something has been mounted. useEffect(() => {...}, []) is executed automatically upon mounting, and you can put whatever needs to wait until mounting inside that effect.
Besides prop value updates in a hook, I need to bind to events that get triggered in the hook too. So the consumer of the hook can bind to the event-like addEventListner, removeEventListener. How do I do this?
What I have so far:
import {useState, useEffect} from 'react';
interface MyHookProps {
name: string;
onChange: () => void;
}
const useNameHook = () : MyHookProps => {
const [name, setName] = useState<string>('Anakin');
const onChange = () => {
}
useEffect(() => {
setTimeout(() => {
setName('Vader');
// how to a raise an onChange event here that consumers could bind to?
}, 1000);
}, []);
return {
name,
onChange,
}
}
export default function App() {
const {name, onChange} = useNameHook();
const handleHookChange = () => {
console.info('hook changed', name);
}
return (
<div className="App">
<h1>Hello {name}</h1>
</div>
);
}
I think you can refer to the 'Declarative' pattern here.
Reading this article about 'Making setInterval Declarative with React Hooks' from Dan Abramov really changed my ways of thinking about the React hooks.
https://overreacted.io/making-setinterval-declarative-with-react-hooks/
So, my attempts to make this useName hook declarative is like below:
// hooks/useName.ts
import { useEffect, useRef, useState } from "react";
type Callback = (name: string) => void;
const useName: (callback: Callback, active: boolean) => string = (
callback,
active
) => {
// "Listener"
const savedCallbackRef = useRef<Callback>();
// keep the listener fresh
useEffect(() => {
savedCallbackRef.current = callback;
}, [callback]);
// name state
const [internalState, setInternalState] = useState("anakin");
// change the name after 1 sec
useEffect(() => {
const timeoutID = setTimeout(() => {
setInternalState("vader");
}, 1000);
return () => clearTimeout(timeoutID);
}, []);
// react to the 'name change event'
useEffect(() => {
if (active) {
savedCallbackRef.current?.(internalState);
}
}, [active, internalState]);
return internalState;
};
export default useName;
and you can use this hook like this:
// App.ts
import useName from "./hooks/useName";
function App() {
const name = useName(state => {
console.log(`in name change event, ${state}`);
}, true);
return <p>{name}</p>;
}
export default App;
Note that the 'callback' runs even with the initial value ('anakin' in this case), and if you want to avoid it you may refer to this thread in SO:
Make React useEffect hook not run on initial render
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;
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 }));