setTimeout is not reset in React JS - reactjs

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

Related

Hide DropDown when clicked outside in Next Js

just started to learn Next Js. I wanna hide dropdown when i clicked outside the button. Code works fine in create-react-app. But i tried to implement in nextjs, it doesnt working.
const LanguageRef = useRef();
const [languageDD, setLanguageDD] = useState(false);
console.log(languageDD);
useEffect(() => {
if (!languageDD) return;
const checkIfClickedOutside = (e) => {
if (
languageDD &&
LanguageRef.current &&
!LanguageRef.current.contains(e.target)
) {
setLanguageDD(false);
}
};
document.addEventListener("click", checkIfClickedOutside);
return () => {
// Cleanup the event listener
document.removeEventListener("click", checkIfClickedOutside);
};
}, [languageDD]);
link tag
<a onClick={() => setLanguageDD((prev) => !prev)}>Language </a>
Does useEffect work in Nextjs?
Working Solution:
const LanguageRef = useRef();
const LanguageDDRef = useRef();
const [languageDD, setLanguageDD] = useState(false);
console.log(languageDD);
useEffect(() => {
console.log("useeffect")
if (!languageDD) return;
const checkIfClickedOutside = (e) => {
if (
languageDD &&
LanguageRef.current &&
!LanguageRef.current.contains(e.target) &&
LanguageDDRef.current &&
!LanguageDDRef.current.contains(e.target)
) {
setLanguageDD(false);
}
};
document.addEventListener("click", checkIfClickedOutside);
return () => {
// Cleanup the event listener
document.removeEventListener("click", checkIfClickedOutside);
};
}, [languageDD]);
<a onClick={() => setLanguageDD((prev) => !prev) ref={LanguageDDRef}}>Language </a>
I reworked and also splitted the clickOutside function to a Custom Hook, because it can reuse at the other components. Like this:
import React, { useEffect } from 'react';
export const useClickOutside = (ref, handler) => {
useEffect(() => {
const listener = (e) => {
// Do nothing if clicking ref's element or descendent elements
if (!ref?.current || ref.current.contains(e.target)) {
return;
}
handler();
};
document.addEventListener('mousedown', listener);
document.addEventListener('touchstart', listener);
return () => {
document.removeEventListener('mousedown', listener);
document.addEventListener('touchstart', listener);
};
}, [handler, ref]);
};
Finally, you just need import and use useClickOutside hook, like this:
import React, { useRef, useState } from 'react';
import { useClickOutside } from '../src/hooks';
export default function Home() {
const LanguageDDRef = useRef();
const [languageDD, setLanguageDD] = useState(true);
useClickOutside(LanguageDDRef, () => setLanguageDD(false));
const handleToggleButton = () => {
setLanguageDD((prev) => !prev);
};
return (
<button onClick={handleToggleButton} ref={LanguageDDRef}>
{languageDD ? 'Show' : 'Hide'}
</button>
);
}

Invalid hook call.Hooks can be called inside of the body. I gets this error

const startTimer = () => {
Alert.alert('hye timer started')
useInterval(() => {
handleTimer()
}, 1000);
};
this is function which is called when i press button.
the useInterval is custom hook who's code is as follow:
import { useEffect, useRef } from 'react';
export default function 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) {
const id = setInterval(tick, delay);
return () => clearInterval(id);
}
}, [delay]);
}
but when i press the button the function is called in which there is a hook which should be called but instead it give an error invalid custom hook. can you tell what is wrong in this code.
You are trying to access a hook inside of a function. Thus, the following is not valid.
function foo() {
useSomeHook();
}
A hook can only be called
[...] from React function components [... and ...]
from custom Hooks
as defined by the rules of hooks. The function startTimer is a regular Javascript function and neither a react functional component nor a custom hook.
For your particular case, you could define the startTimer function inside the hook itself and export it. Then, call it onPress.
export default function useInterval(callback, delay) {
const savedCallback = useRef();
const [start, setStart] = useState(false)
// Remember the latest callback.
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
// Set up the interval.
useEffect(() => {
if (start) {
const tick = () => {
savedCallback.current();
}
if (delay !== null) {
const id = setInterval(tick, delay);
return () => clearInterval(id);
}
}
}, [delay, start]);
const startTimer = React.useCallback((shouldStart) => {
setStart(shouldStart)
}, []);
return startTimer;
}
You can start and stop it as follows:
export default function App() {
const startTimer = useInterval(() => console.log("callback"), 1000)
return (
<View style={{margin:50}}>
<Pressable onPress={() => startTimer(true)}> <Text>Start</Text></Pressable>
<Pressable onPress={() => startTimer(false)}> <Text>Stop</Text></Pressable>
</View>
);
}

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.

Resources