Debouncing and Timeout in React - javascript

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.

Related

UseEffect runs on first render without being called using Context [duplicate]

According to the docs:
componentDidUpdate() is invoked immediately after updating occurs. This method is not called for the initial render.
We can use the new useEffect() hook to simulate componentDidUpdate(), but it seems like useEffect() is being ran after every render, even the first time. How do I get it to not run on initial render?
As you can see in the example below, componentDidUpdateFunction is printed during the initial render but componentDidUpdateClass was not printed during the initial render.
function ComponentDidUpdateFunction() {
const [count, setCount] = React.useState(0);
React.useEffect(() => {
console.log("componentDidUpdateFunction");
});
return (
<div>
<p>componentDidUpdateFunction: {count} times</p>
<button
onClick={() => {
setCount(count + 1);
}}
>
Click Me
</button>
</div>
);
}
class ComponentDidUpdateClass extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0,
};
}
componentDidUpdate() {
console.log("componentDidUpdateClass");
}
render() {
return (
<div>
<p>componentDidUpdateClass: {this.state.count} times</p>
<button
onClick={() => {
this.setState({ count: this.state.count + 1 });
}}
>
Click Me
</button>
</div>
);
}
}
ReactDOM.render(
<div>
<ComponentDidUpdateFunction />
<ComponentDidUpdateClass />
</div>,
document.querySelector("#app")
);
<script src="https://unpkg.com/react#16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16.7.0-alpha.0/umd/react-dom.development.js"></script>
<div id="app"></div>
We can use the useRef hook to store any mutable value we like, so we could use that to keep track of if it's the first time the useEffect function is being run.
If we want the effect to run in the same phase that componentDidUpdate does, we can use useLayoutEffect instead.
Example
const { useState, useRef, useLayoutEffect } = React;
function ComponentDidUpdateFunction() {
const [count, setCount] = useState(0);
const firstUpdate = useRef(true);
useLayoutEffect(() => {
if (firstUpdate.current) {
firstUpdate.current = false;
return;
}
console.log("componentDidUpdateFunction");
});
return (
<div>
<p>componentDidUpdateFunction: {count} times</p>
<button
onClick={() => {
setCount(count + 1);
}}
>
Click Me
</button>
</div>
);
}
ReactDOM.render(
<ComponentDidUpdateFunction />,
document.getElementById("app")
);
<script src="https://unpkg.com/react#16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16.7.0-alpha.0/umd/react-dom.development.js"></script>
<div id="app"></div>
You can turn it into custom hooks, like so:
import React, { useEffect, useRef } from 'react';
const useDidMountEffect = (func, deps) => {
const didMount = useRef(false);
useEffect(() => {
if (didMount.current) func();
else didMount.current = true;
}, deps);
}
export default useDidMountEffect;
Usage example:
import React, { useState, useEffect } from 'react';
import useDidMountEffect from '../path/to/useDidMountEffect';
const MyComponent = (props) => {
const [state, setState] = useState({
key: false
});
useEffect(() => {
// you know what is this, don't you?
}, []);
useDidMountEffect(() => {
// react please run me if 'key' changes, but not on initial render
}, [state.key]);
return (
<div>
...
</div>
);
}
// ...
I made a simple useFirstRender hook to handle cases like focussing a form input:
import { useRef, useEffect } from 'react';
export function useFirstRender() {
const firstRender = useRef(true);
useEffect(() => {
firstRender.current = false;
}, []);
return firstRender.current;
}
It starts out as true, then switches to false in the useEffect, which only runs once, and never again.
In your component, use it:
const firstRender = useFirstRender();
const phoneNumberRef = useRef(null);
useEffect(() => {
if (firstRender || errors.phoneNumber) {
phoneNumberRef.current.focus();
}
}, [firstRender, errors.phoneNumber]);
For your case, you would just use if (!firstRender) { ....
Same approach as Tholle's answer, but using useState instead of useRef.
const [skipCount, setSkipCount] = useState(true);
...
useEffect(() => {
if (skipCount) setSkipCount(false);
if (!skipCount) runYourFunction();
}, [dependencies])
EDIT
While this also works, it involves updating state which will cause your component to re-render. If all your component's useEffect calls (and also all of its children's) have a dependency array, this doesn't matter. But keep in mind that any useEffect without a dependency array (useEffect(() => {...}) will be run again.
Using and updating useRef will not cause any re-renders.
#ravi, yours doesn't call the passed-in unmount function. Here's a version that's a little more complete:
/**
* Identical to React.useEffect, except that it never runs on mount. This is
* the equivalent of the componentDidUpdate lifecycle function.
*
* #param {function:function} effect - A useEffect effect.
* #param {array} [dependencies] - useEffect dependency list.
*/
export const useEffectExceptOnMount = (effect, dependencies) => {
const mounted = React.useRef(false);
React.useEffect(() => {
if (mounted.current) {
const unmount = effect();
return () => unmount && unmount();
} else {
mounted.current = true;
}
}, dependencies);
// Reset on unmount for the next mount.
React.useEffect(() => {
return () => mounted.current = false;
}, []);
};
a simple way is to create a let, out of your component and set in to true.
then say if its true set it to false then return (stop) the useEffect function
like that:
import { useEffect} from 'react';
//your let must be out of component to avoid re-evaluation
let isFirst = true
function App() {
useEffect(() => {
if(isFirst){
isFirst = false
return
}
//your code that don't want to execute at first time
},[])
return (
<div>
<p>its simple huh...</p>
</div>
);
}
its Similar to #Carmine Tambasciabs solution but without using state :)
‍‍‍‍‍‍
‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍
function useEffectAfterMount(effect, deps) {
const isMounted = useRef(false);
useEffect(() => {
if (isMounted.current) return effect();
else isMounted.current = true;
}, deps);
// reset on unmount; in React 18, components can mount again
useEffect(() => {
isMounted.current = false;
});
}
We need to return what comes back from effect(), because it might be a cleanup function. But we don't need to determine if it is or not. Just pass it on and let useEffect figure it out.
In an earlier version of this post I said resetting the ref (isMounted.current = false) wasn't necessary. But in React 18 it is, because components can remount with their previous state (thanks #Whatabrain).
I thought creating a custom hook would be overkill and I didn't want to muddle my component's readability by using the useLayoutEffect hook for something unrelated to layouts, so, in my case, I simply checked to see if the value of my stateful variable selectedItem that triggers the useEffect callback is its original value in order to determine if it's the initial render:
export default function MyComponent(props) {
const [selectedItem, setSelectedItem] = useState(null);
useEffect(() => {
if(!selectedItem) return; // If selected item is its initial value (null), don't continue
//... This will not happen on initial render
}, [selectedItem]);
// ...
}
This is the best implementation I've created so far using typescript. Basically, the idea is the same, using the Ref but I'm also considering the callback returned by useEffect to perform cleanup on component unmount.
import {
useRef,
EffectCallback,
DependencyList,
useEffect
} from 'react';
/**
* #param effect
* #param dependencies
*
*/
export default function useNoInitialEffect(
effect: EffectCallback,
dependencies?: DependencyList
) {
//Preserving the true by default as initial render cycle
const initialRender = useRef(true);
useEffect(() => {
let effectReturns: void | (() => void) = () => {};
// Updating the ref to false on the first render, causing
// subsequent render to execute the effect
if (initialRender.current) {
initialRender.current = false;
} else {
effectReturns = effect();
}
// Preserving and allowing the Destructor returned by the effect
// to execute on component unmount and perform cleanup if
// required.
if (effectReturns && typeof effectReturns === 'function') {
return effectReturns;
}
return undefined;
}, dependencies);
}
You can simply use it, as usual as you use the useEffect hook but this time, it won't run on the initial render. Here is how you can use this hook.
useNoInitialEffect(() => {
// perform something, returning callback is supported
}, [a, b]);
If you use ESLint and want to use the react-hooks/exhaustive-deps rule for this custom hook:
{
"rules": {
// ...
"react-hooks/exhaustive-deps": ["warn", {
"additionalHooks": "useNoInitialEffect"
}]
}
}
#MehdiDehghani, your solution work perfectly fine, one addition you have to do is on unmount, reset the didMount.current value to false. When to try to use this custom hook somewhere else, you don't get cache value.
import React, { useEffect, useRef } from 'react';
const useDidMountEffect = (func, deps) => {
const didMount = useRef(false);
useEffect(() => {
let unmount;
if (didMount.current) unmount = func();
else didMount.current = true;
return () => {
didMount.current = false;
unmount && unmount();
}
}, deps);
}
export default useDidMountEffect;
Simplified implementation
import { useRef, useEffect } from 'react';
function MyComp(props) {
const firstRender = useRef(true);
useEffect(() => {
if (firstRender.current) {
firstRender.current = false;
} else {
myProp = 'some val';
};
}, [props.myProp])
return (
<div>
...
</div>
)
}
You can use custom hook to run use effect after mount.
const useEffectAfterMount = (cb, dependencies) => {
const mounted = useRef(true);
useEffect(() => {
if (!mounted.current) {
return cb();
}
mounted.current = false;
}, dependencies); // eslint-disable-line react-hooks/exhaustive-deps
};
Here is the typescript version:
const useEffectAfterMount = (cb: EffectCallback, dependencies: DependencyList | undefined) => {
const mounted = useRef(true);
useEffect(() => {
if (!mounted.current) {
return cb();
}
mounted.current = false;
}, dependencies); // eslint-disable-line react-hooks/exhaustive-deps
};
For people who are having trouble with React 18 strict mode calling the useeffect on the initial render twice, try this:
// The init variable is necessary if your state is an object/array, because the == operator compares the references, not the actual values.
const init = [];
const [state, setState] = useState(init);
const dummyState = useRef(init);
useEffect(() => {
// Compare the old state with the new state
if (dummyState.current == state) {
// This means that the component is mounting
} else {
// This means that the component updated.
dummyState.current = state;
}
}, [state]);
Works in development mode...
function App() {
const init = [];
const [state, setState] = React.useState(init);
const dummyState = React.useRef(init);
React.useEffect(() => {
if (dummyState.current == state) {
console.log('mount');
} else {
console.log('update');
dummyState.current = state;
}
}, [state]);
return (
<button onClick={() => setState([...state, Math.random()])}>Update state </button>
);
}
ReactDOM.createRoot(document.getElementById("app")).render(
<React.StrictMode>
<App />
</React.StrictMode>
);
<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="app"></div>
And in production.
function App() {
const init = [];
const [state, setState] = React.useState(init);
const dummyState = React.useRef(init);
React.useEffect(() => {
if (dummyState.current == state) {
console.log('mount');
} else {
console.log('update');
dummyState.current = state;
}
}, [state]);
return (
<button onClick={() => setState([...state, Math.random()])}>Update state </button>
);
}
ReactDOM.createRoot(document.getElementById("app")).render(
<React.StrictMode>
<App />
</React.StrictMode>
);
<script crossorigin src="https://unpkg.com/react#18/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#18/umd/react-dom.production.min.js"></script>
<div id="app"></div>
If you want to skip the first render, you can create a state "firstRenderDone" and set it to true in the useEffect with empty dependecy list (that works like a didMount). Then, in your other useEffect, you can check if the first render was already done before doing something.
const [firstRenderDone, setFirstRenderDone] = useState(false);
//useEffect with empty dependecy list (that works like a componentDidMount)
useEffect(() => {
setFirstRenderDone(true);
}, []);
// your other useEffect (that works as componetDidUpdate)
useEffect(() => {
if(firstRenderDone){
console.log("componentDidUpdateFunction");
}
}, [firstRenderDone]);
All previous are good, but this can be achieved in a simplier way considering that the action in useEffect can be "skipped" placing an if condition(or any other ) that is basically not run first time, and still with the dependency.
For example I had the case of :
Load data from an API but my title has to be "Loading" till the date were not there, so I have an array, tours that is empty at beginning and show the text "Showing"
Have a component rendered with different information from those API.
The user can delete one by one those info, even all making the tour array empty again as the beginning but this time the API fetch is been already done
Once the tour list is empty by deleting then show another title.
so my "solution" was to create another useState to create a boolean value that change only after the data fetch making another condition in useEffect true in order to run another function that also depend on the tour length.
useEffect(() => {
if (isTitle) {
changeTitle(newTitle)
}else{
isSetTitle(true)
}
}, [tours])
here my App.js
import React, { useState, useEffect } from 'react'
import Loading from './Loading'
import Tours from './Tours'
const url = 'API url'
let newTours
function App() {
const [loading, setLoading ] = useState(true)
const [tours, setTours] = useState([])
const [isTitle, isSetTitle] = useState(false)
const [title, setTitle] = useState("Our Tours")
const newTitle = "Tours are empty"
const removeTours = (id) => {
newTours = tours.filter(tour => ( tour.id !== id))
return setTours(newTours)
}
const changeTitle = (title) =>{
if(tours.length === 0 && loading === false){
setTitle(title)
}
}
const fetchTours = async () => {
setLoading(true)
try {
const response = await fetch(url)
const tours = await response.json()
setLoading(false)
setTours(tours)
}catch(error) {
setLoading(false)
console.log(error)
}
}
useEffect(()=>{
fetchTours()
},[])
useEffect(() => {
if (isTitle) {
changeTitle(newTitle)
}else{
isSetTitle(true)
}
}, [tours])
if(loading){
return (
<main>
<Loading />
</main>
)
}else{
return (
<main>
<Tours tours={tours} title={title} changeTitle={changeTitle}
removeTours={removeTours} />
</main>
)
}
}
export default App
const [dojob, setDojob] = useState(false);
yourfunction(){
setDojob(true);
}
useEffect(()=>{
if(dojob){
yourfunction();
setDojob(false);
}
},[dojob]);

Why `setTimeout` call more than one time when I use `useState`?

I'm so confused about useState in React hooks.
I do not know why console.log in setTimeout function calls more than one time when I use useState.
If I remove useState it normally calls only once.
And If I use Class state instead hooks, it normally calls only once as well.
Why is it happened that ?
And how can I handle it ?
(here is my code)
import React, { useState, useEffect } from "react";
import "./App.css";
const usePassword = () => {
const [passwordValue, setPasswordValue] = useState({
password: "",
passwordHidden: "",
});
let timer = null;
const trigger = () => {
clearTimeout(timer);
timer = setTimeout(() => console.log("end"), 1000);
};
const onPasswordChanged = (name, value) => {
setPasswordValue((prev) => ({ ...passwordValue, passwordHidden: value }));
trigger();
};
return { passwordValue, onPasswordChanged };
};
function App() {
const { passwordValue, onPasswordChanged } = usePassword();
const onChanged = (event) => {
const { name, value } = event.target;
onPasswordChanged(name, value);
};
const onSubmit = () => {
console.log("submitted!", passwordValue);
};
return (
<div className="App">
<header className="App-header">
<input name="password" onKeyUp={onChanged} />
<button onClick={onSubmit}>Submit</button>
</header>
</div>
);
}
export default App;
Whenever you set the state using useState you get a new timer variable, as the function is called again. This is why your clearTimeout does not work.
You can use a ref to hold on to the value between render cycles:
const timer = useRef(null);
const trigger = () => {
clearTimeout(timer.current);
timer.current = setTimeout(() => console.log("end"), 1000);
};

How to make javascript debounce work in react

I'm having trouble using debounce method in react which works perfectly in javascript when we set input field onInput to a function , like , using setSearchData function , <input type='text onInput='setSearchData ()'/>
but the same debounce logic doesn't work . I don't get any text on my textarea nor do I get any console.log
My debounce logic consists of setSearchData ,searchData2 and searchData3 functions.
But here I think since I'm using event I can't get any value. I've also tried using
document.getElementById('texx).addEventListener('input',setSearchData,false)
import React, { useEffect, useState } from 'react'
import { TextField, Grid, Box, makeStyles, Toolbar, createMuiTheme, ThemeProvider, Container, AppBar, Divider, Card, CardMedia } from '#material-ui/core'
import PrimarySearchAppBar from '../Appbar'
import axios from 'axios';
import Wearcard from './Wearcard';
const useStyles = makeStyles({
})
const Weather = () => {
const [search, setSearch] = useState('')
const [city, setCity] = useState([])
const [addCity, setaddCity] = useState([])
const fetchLocat = async () => {
try {
const { data } = await axios.get(`https://api.openweathermap.org/data/2.5/weather?q=${search}&appid=${process.env.REACT_APP_API_KEY}`)
console.log(data)
setCity(data)
} catch (error) {
console.error(error)
}
}
// debounce method for search hook
// document.getElementById('texx').addEventListener('input', setSearchData, false)
**const searchData3 = (event) => {
setSearch(event.target.value)
let ds = 1;
console.log('wow', ds++)
}
const searchData2 = (func, delay) => {
let timer;
return function () {
let context = this;
let argss = arguments;
clearTimeout(timer)
timer = setTimeout(() => {
searchData3.apply(context, arguments);
}, delay)
}
}
const setSearchData = searchData2(searchData3, 1200);**
useEffect(() => {
fetchLocat()
}, [search])
console.log(this)
return (
<div>
<PrimarySearchAppBar color='secondary' />
<Toolbar />
<Container>
<Divider />
<br />
**<TextField label='Enter pin code' id='texx' className='searchhold' value={search} variant='filled' size='medium' type='search' name='cityName' onInput={setSearchData}>**
</TextField>
<br /><br /><br /><br />
<Grid>
<Wearcard />
</Grid>
</Container>
</div >)
}
export default Weather;
As you said, you are using like, This will not work
<input type='text onInput='setSearchData ()'/>
To Make it work, Update like
<input type='text onInput={setSearchData}/>
Clean and reusable approach
const debounce = (func, delay) => {
let timer;
return function (...args) {
let context = this;
clearTimeout(timer);
timer = setTimeout(() => func.apply(context, args), delay);
};
};
const searchData3 = (event) => {
setSearch(event.target.value);
let ds = 1;
console.log("wow", ds++);
};
const setSearchData = debounce(searchData3, 1200);
Aside from the answer above,
You may use lodash, which done the debounce function for you
import delay from "lodash/debounce";
const delay = /* some number */;
const searchData3 = event => debounce(() => {
setSearch(event.target.value);
}, delay);
React 18 (still in alpha) now support this natively and much better than setTimeout.
import { startTransition } from "react"
const searchData3 = event => startTransition(() => {
setSearch(event.target.value); // this will run in lower priority then other setState
});

How to disappear alert after 5 seconds in React JS?

import { useState } from 'react'
const Message = ({ variant, children }) => {
const [timeOut, setTimeOut] = useState(null)
setTimeout(() => {
setTimeOut(1)
}, 3000)
return (
timeOut !== 1 && <div className={`alert alert-${variant}`}>{children}</div>
)
}
Message.defaultPros = {
variant: 'info',
}
export default Message
I want to disappear this alert after 2 or 3 seconds. I used this logic it's fine and working but In my console, I'm having this warning:
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.
Is it affecting my app or is it oka? You can give me a better idea to implement this logic.
You can read through the comments
import { useState, useEffect } from 'react'
const Message = ({ variant, children }) => {
const [show, setShow] = useState(true)
// On componentDidMount set the timer
useEffect(() => {
const timeId = setTimeout(() => {
// After 3 seconds set the show value to false
setShow(false)
}, 3000)
return () => {
clearTimeout(timeId)
}
}, []);
// If show is false the component will return null and stop here
if (!show) {
return null;
}
// If show is true this will be returned
return (
<div className={`alert alert-${variant}`}>
{children}
</div>
)
}
Message.defaultPros = {
variant: 'info',
}
export default Message;
This will show the alert during 3 seconds, then it will disappear :
import React, { useEffect, useState } from 'react';
const Message = ({ variant, children }) => {
// the alert is displayed by default
const [alert, setAlert] = useState(true);
useEffect(() => {
// when the component is mounted, the alert is displayed for 3 seconds
setTimeout(() => {
setAlert(false);
}, 3000);
}, []);
return (
{alert && <div className={`alert alert-${variant}`}>{children}</div>}
)
}
If you use MUI, then you can make alerts appear and disapper with fade in transitions.
Here's the code
import Alert from "#mui/material/Alert";
import AlertTitle from "#mui/material/AlertTitle";
import Fade from "#mui/material/Fade";
export default function Registration() {
const [alertVisibility, setAlertVisibility] = useState(false);
return(
<Fade
in={alertVisibility} //Write the needed condition here to make it appear
timeout={{ enter: 1000, exit: 1000 }} //Edit these two values to change the duration of transition when the element is getting appeared and disappeard
addEndListener={() => {
setTimeout(() => {
setAlertVisibility(true)
}, 2000);
}}
>
<Alert severity="success" variant="standard" className="alert">
<AlertTitle>Success</AlertTitle>
Registration Successful!
</Alert>
</Fade>
)
}
For more transitions refer this -> https://mui.com/material-ui/transitions/
import React, { useEffect, useState } from 'react';
const Message = ({ variant, children }) => {
const [alert, setAlert] = useState(true);
useEffect(() => {
const timer = setTimeout(() => {
setAlert(false);
}, 3000);
// To clear or cancel a timer, you call the clearTimeout(); method,
// passing in the timer object that you created into clearTimeout().
return () => clearTimeout(timer);
}, []);
return (
{alert && <div className={`alert alert-${variant}`}>{children}</div>}
)
}

Using React useEffect hook with rxjs mergeMap operator

I'm trying to implement a data stream that has to use inner observables, where I use one from mergeMap, concatMap etc.
e.g.:
const output$$ = input$$.pipe(
mergeMap(str => of(str).pipe(delay(10))),
share()
);
output$$.subscribe(console.log);
This works fine when logging into console.
But when I try to use it in React like below utilizing useEffect and useState hooks to update some text:
function App() {
const input$ = new Subject<string>();
const input$$ = input$.pipe(share());
const output$$ = input$$.pipe(
mergeMap(str => of(str).pipe(delay(10))),
share()
);
output$$.subscribe(console.log);
// This works
const [input, setInput] = useState("");
const [output, setOutput] = useState("");
useEffect(() => {
const subscription = input$$.subscribe(setInput);
return () => {
subscription.unsubscribe();
};
}, [input$$]);
useEffect(() => {
const subscription = output$$.subscribe(setOutput);
// This doesn't
return () => {
subscription.unsubscribe();
};
}, [output$$]);
return (
<div className="App">
<input
onChange={event => input$.next(event.target.value)}
value={input}
/>
<p>{output}</p>
</div>
);
}
it starts acting weird/unpredictable (e.g.: sometimes the text is updated in the middle of typing, sometimes it doesn't update at all).
Things I have noticed:
If the inner observable completes immediately/is a promise that
resolves immediately, it works fine.
If we print to console instead of useEffect, it works fine.
I believe this has to do something with the inner workings of useEffect and how it captures and notices outside changes, but cannot get it working.
Any help is much appreciated.
Minimal reproduction of the case:
https://codesandbox.io/s/hooks-and-observables-1-7ygd8
I'm not quite sure what you're trying to achieve, but I found a number of problems which hopefully the following code fixes:
function App() {
// Create these observables only once.
const [input$] = useState(() => new Subject<string>());
const [input$$] = useState(() => input$.pipe(share()));
const [output$$] = useState(() => input$$.pipe(
mergeMap(str => of(str).pipe(delay(10))),
share()
));
const [input, setInput] = useState("");
const [output, setOutput] = useState("");
// Create the subscription to input$$ on component mount, not on every render.
useEffect(() => {
const subscription = input$$.subscribe(setInput);
return () => {
subscription.unsubscribe();
};
}, []);
// Create the subscription to output$$ on component mount, not on every render.
useEffect(() => {
const subscription = output$$.subscribe(setOutput);
return () => {
subscription.unsubscribe();
};
}, []);
return (
<div className="App">
<input
onChange={event => input$.next(event.target.value)}
value={input}
/>
<p>{output}</p>
</div>
);
}
I had a similar task but the goal was to pipe and debounce the input test and execute ajax call.
The simple answer that you should init RxJS subject with arrow function in the react hook 'useState' in order to init subject once per init.
Then you should useEffect with empty array [] in order to create a pipe once on component init.
import React, { useEffect, useState } from "react";
import { ajax } from "rxjs/ajax";
import { debounceTime, delay, takeUntil } from "rxjs/operators";
import { Subject } from "rxjs/internal/Subject";
const App = () => {
const [items, setItems] = useState([]);
const [loading, setLoading] = useState(true);
const [filterChangedSubject] = useState(() => {
// Arrow function is used to init Singleton Subject. (in a scope of a current component)
return new Subject<string>();
});
useEffect(() => {
// Effect that will be initialized once on a react component init.
// Define your pipe here.
const subscription = filterChangedSubject
.pipe(debounceTime(200))
.subscribe((filter) => {
if (!filter) {
setLoading(false);
setItems([]);
return;
}
ajax(`https://swapi.dev/api/people?search=${filter}`)
.pipe(
// current running ajax is canceled on filter change.
takeUntil(filterChangedSubject)
)
.subscribe(
(results) => {
// Set items will cause render:
setItems(results.response.results);
},
() => {
setLoading(false);
},
() => {
setLoading(false);
}
);
});
return () => {
// On Component destroy. notify takeUntil to unsubscribe from current running ajax request
filterChangedSubject.next("");
// unsubscribe filter change listener
subscription.unsubscribe();
};
}, []);
const onFilterChange = (e) => {
// Notify subject about the filter change
filterChangedSubject.next(e.target.value);
};
return (
<div>
Cards
{loading && <div>Loading...</div>}
<input onChange={onFilterChange}></input>
{items && items.map((item, index) => <div key={index}>{item.name}</div>)}
</div>
);
};
export default App;

Categories