Debounce in React component JS - javascript

I'm writing a simple debounce function for an input component
export const debounce = (func, wait) => {
let timeout
return function (...args) {
if (timeout) {
clearTimeout(timeout)
}
timeout = setTimeout(() => {
timeout = null
Reflect.apply(func, this, args)
}, wait)
}
}
it imported from an external file, and used as a wrap for input onKeyUp handler inside a React component (Hooks)
const handleChange = debounce(() => console.log("test"), 1000)
PROBLEM: I'm getting "test" log every time when text in input changes, not only one - as expected.
What am I doing wrong?

I'm not sure what is the problem with your code but here is a version with hooks working
import { useEffect, useState } from "react";
const useDebounce = (value, delay) => {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const handler = setTimeout(() => setDebouncedValue(value), delay);
return () => clearTimeout(handler);
}, [value, delay]);
return debouncedValue;
};
export default useDebounce;
and then you use it as
const debouncedValue = useDebounce(inputValue, delay);

Related

Assignments to the 'timeInterval' variable from inside React Hook useEffect will be lost after each render

I need the auto re-rendering every 6s and I defined the component like the following.
const KioskPage = () => {
const [time, setTime] = useState(Date.now())
useEffect(() => {
timeInterval = setInterval(() => setTime(Date.now()), 60000)
return () => {
clearInterval(timeInterval)
}
}, [])
}
but I got the notification :
Assignments to the 'timeInterval' variable from inside React Hook useEffect will be lost after each render. To preserve the value over time, store it in a useRef Hook and keep the mutable value in the '.current' property. Otherwise, you can move this variable directly inside useEffect react-hooks/exhaustive-deps
Why this happen? and How can I fix this issue?
Regards
Assignments to the 'timeInterval' variable from inside React Hook useEffect will be lost after each render.
To illustrate:
const KioskPage = () => {
const [time, setTime] = useState(Date.now())
let timeInterval;
// ^^^^^ this piece of code gets run on each render, when state/prop changes.
// this will, in practice, clear the `timeInterval` value set by your effect below, when the component re-renders after being mounted.
useEffect(() => {
timeInterval = setInterval(() => setTime(Date.now()), 60000)
// ^^^^ this assignment gets run once when the component mounts
return () => {
clearInterval(timeInterval)
}
}, []);
return (/* render something*/);
}
You can fix this by, as suggested, "move this variable directly inside useEffect".
const KioskPage = () => {
const [time, setTime] = useState(Date.now())
useEffect(() => {
const timeInterval = setInterval(() => setTime(Date.now()), 6000);
// ^^^^^^^^^^^^^^^
return () => {
console.log('clearing!');
clearInterval(timeInterval)
}
}, []);
const formatted = new Date(time).toLocaleTimeString();
return (
<h1>Time: {formatted}</h1>
);
}

React infity loop on function call

i've a (certainly) stupid problem.
my function getDataTbable is called in infinity loop I don't understand why... So the request is infinitive called.
export const TableResearch = ({setSelectedSuggestion,setImages}) => {
const [research, setResearch] = useState('');
const [suggestions, setSuggestions] = useState ([]);
const [table, setTable]= useState ([]);
const getDataTable = async () => {
const {data} = await jsonbin.get('/b/5f3d58e44d93991036184474');
setTable(data);
console.log(table)
};
getDataTable();
That is because the TableResearch function is called multiple times (every time this component is rendered). If you want to run a function only when the component is mounted, you'll have to use useEffect. Here is an example:
useEffect(() => {
const {data} = await jsonbin.get('/b/5f3d58e44d93991036184474');
setTable(data);
}, []);
The second parameter [] passed to useEffect is important. It makes the function run only once.
You can learn more about useEffect from HERE
The component re-renders every time you change it's state (setTable).
You should use useEffect to only execute your function the first time it renders.
Also you might encounter this warning:
Warning: Can't perform a React state update on an unmounted component.
if the async call finishes after component has unmounted. To account for that, write useEffect like this:
// outside component
const getDataTable = async () => {
const { data } = await jsonbin.get("/b/5f3d58e44d93991036184474");
return data;
};
useEffect(() => {
let mounted = true;
getDataTable()
.then(data => {
if (!mounted) return;
setTable(data);
});
return () => {
mounted = false;
};
}, []);
You can try this
useEffect(() => {
const getDataTable = async () => {
const { data } = await jsonbin.get("/b/5f3d58e44d93991036184474");
setTable(data);
console.log(table);
};
getDataTable();
}, []); // [] makes it runs once
Yes, getDataTable(); is being executed everytime the view is rendered, including when the data is returned.
Try wrapping getDataTable() like this:
if (!table.length) {
getDataTable()
}
But you will need to handle the case for if the requests returns no results, in which case it will still run infinitely:
const [table, setTable]= useState([]);
const [loading, setLoading]= useState();
const getDataTable = async () => {
setLoading(true);
const {data} = await jsonbin.get('/b/5f3d58e44d93991036184474');
setTable(data);
setLoading(false);
};
if (typeof loading === 'undefined' && !table.length) {
getDataTable();
}

How to prevent inline functions from binding to old state values

In a project with React components using hooks, I am trying to understand how to properly avoid calling callbacks that are bound to old state values. The below example illustrates the issue (but is not the code I am working on).
import React, { useState, useEffect } from "react";
import ReactDOM from "react-dom";
const Message = () => {
const [message, setMessage] = useState("");
function doStuff() {
console.log(message);
}
useEffect(() => {
setInterval(doStuff, 1000)
}, []);
return (
<div>
<input
type="text"
value={message}
placeholder="Enter a message"
onChange={e => setMessage(e.target.value)}
/>
<p>
<strong>{message}</strong>
</p>
</div>
);
};
const rootElement = document.getElementById("root");
ReactDOM.render(<Message />, rootElement);
The problem here is of course that setInterval will keep the doStuff function as it was when the effect was called the first (and only time). And at that time the message state was empty and hence, the interval function will print an empty string every second instead of the message that is actually inside the text box.
In my real code, I am having external events that should trigger function calls inside the component, and they suffer this same issue.
What should I do?
You should useCallback and pass it as a dependency to your effect.
const doStuff = useCallback(() => {
console.log(message);
}, [message]);
useEffect(() => {
const interval = setInterval(doStuff, 1000);
return () => clearInterval(interval); // clean up
}, [doStuff]);
Here when message gets updated it will have its new value in the doStuff
You can put this in it's own hook also. I have this in my production code
/**
* A react hook for setting up an interval
* #param handler - Function to execute on interval
* #param interval - interval in milliseconds
* #param runImmediate - If the function is executed immediately
*/
export const useInterval = (handler: THandlerFn, interval: number | null, runImmediate = false): void => {
const callbackFn = useRef<THandlerFn>()
// Update callback function
useEffect((): void => {
callbackFn.current = handler
}, [handler])
// Setup interval
useEffect((): (() => void) | void => {
const tick = (): void => {
callbackFn.current && callbackFn.current()
}
let timerId: number
if (interval) {
if (runImmediate) {
setTimeout(tick, 0)
}
timerId = setInterval(tick, interval)
return (): void => {
clearInterval(timerId)
}
}
}, [interval, runImmediate])
}

Issues triggering Modal to show inside useEffect hook [duplicate]

I get this error:
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.
when fetching of data is started and component was unmounted, but function is trying to update state of unmounted component.
What is the best way to solve this?
CodePen example.
default function Test() {
const [notSeenAmount, setNotSeenAmount] = useState(false)
useEffect(() => {
let timer = setInterval(updateNotSeenAmount, 2000)
return () => clearInterval(timer)
}, [])
async function updateNotSeenAmount() {
let data // here i fetch data
setNotSeenAmount(data) // here is problem. If component was unmounted, i get error.
}
async function anotherFunction() {
updateNotSeenAmount() //it can trigger update too
}
return <button onClick={updateNotSeenAmount}>Push me</button> //update can be triggered manually
}
The easiest solution is to use a local variable that keeps track of whether the component is mounted or not. This is a common pattern with the class based approach. Here is an example that implement it with hooks:
function Example() {
const [text, setText] = React.useState("waiting...");
React.useEffect(() => {
let isCancelled = false;
simulateSlowNetworkRequest().then(() => {
if (!isCancelled) {
setText("done!");
}
});
return () => {
isCancelled = true;
};
}, []);
return <h2>{text}</h2>;
}
Here is an alternative with useRef (see below). Note that with a list of dependencies this solution won't work. The value of the ref will stay true after the first render. In that case the first solution is more appropriate.
function Example() {
const isCancelled = React.useRef(false);
const [text, setText] = React.useState("waiting...");
React.useEffect(() => {
fetch();
return () => {
isCancelled.current = true;
};
}, []);
function fetch() {
simulateSlowNetworkRequest().then(() => {
if (!isCancelled.current) {
setText("done!");
}
});
}
return <h2>{text}</h2>;
}
You can find more information about this pattern inside this article. Here is an issue inside the React project on GitHub that showcase this solution.
If you are fetching data from axios(using hooks) and the error still occurs, just wrap the setter inside the condition
let isRendered = useRef(false);
useEffect(() => {
isRendered = true;
axios
.get("/sample/api")
.then(res => {
if (isRendered) {
setState(res.data);
}
return null;
})
.catch(err => console.log(err));
return () => {
isRendered = false;
};
}, []);
TL;DR
Here is a CodeSandBox example
The other answers work of course, I just wanted to share a solution I came up with.
I built this hook that works just like React's useState, but will only setState if the component is mounted. I find it more elegant because you don't have to mess arround with an isMounted variable in your component !
Installation :
npm install use-state-if-mounted
Usage :
const [count, setCount] = useStateIfMounted(0);
You can find more advanced documentation on the npm page of the hook.
Here is a simple solution for this. This warning is due to when we do some fetch request while that request is in the background (because some requests take some time.)and we navigate back from that screen then react cannot update the state. here is the example code for this. write this line before every state Update.
if(!isScreenMounted.current) return;
Here is Complete Example
import React , {useRef} from 'react'
import { Text,StatusBar,SafeAreaView,ScrollView, StyleSheet } from 'react-native'
import BASEURL from '../constants/BaseURL';
const SearchScreen = () => {
const isScreenMounted = useRef(true)
useEffect(() => {
return () => isScreenMounted.current = false
},[])
const ConvertFileSubmit = () => {
if(!isScreenMounted.current) return;
setUpLoading(true)
var formdata = new FormData();
var file = {
uri: `file://${route.params.selectedfiles[0].uri}`,
type:`${route.params.selectedfiles[0].minetype}`,
name:`${route.params.selectedfiles[0].displayname}`,
};
formdata.append("file",file);
fetch(`${BASEURL}/UploadFile`, {
method: 'POST',
body: formdata,
redirect: 'manual'
}).then(response => response.json())
.then(result => {
if(!isScreenMounted.current) return;
setUpLoading(false)
}).catch(error => {
console.log('error', error)
});
}
return(
<>
<StatusBar barStyle="dark-content" />
<SafeAreaView>
<ScrollView
contentInsetAdjustmentBehavior="automatic"
style={styles.scrollView}>
<Text>Search Screen</Text>
</ScrollView>
</SafeAreaView>
</>
)
}
export default SearchScreen;
const styles = StyleSheet.create({
scrollView: {
backgroundColor:"red",
},
container:{
flex:1,
justifyContent:"center",
alignItems:"center"
}
})
This answer is not related to the specific question but I got the same 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. and as a React newcomer could not find a solution to it.
My problem was related to useState in an unmounted component.
I noticed that I was calling a set state function (setIsLoading) after the function that unmounted my component:
const Login = () => {
const [isLoading, setIsLoading] = useState(false);
const handleLogin = () => {
setIsLoading(true);
firebase.auth().then(
functionToUnMountLoginSection();
// the problem is here
setIsLoading(false);
)
}
}
The correct way is to call setIsLoading when the component is still mounted, before calling the function to unmount/process user login in my specific case:
firebase.auth().then(
setIsLoading(false);
functionToUnMountLoginSection();
)
You add the state related datas into the useEffect body for not rerunning them every rerendering process. This method will solve the problem.
useEffect(() => {
let timer = setInterval(updateNotSeenAmount, 2000)
return () => clearInterval(timer)
}, [notSeenAmount])
REF: Tip: Optimizing Performance by Skipping Effects
Custom Hook Solution (ReactJs/NextJs)
Create a new folder named 'shared' and add two folders named 'hooks', 'utils' in it. Add a new file called 'commonFunctions.js' inside utils folder and add the code snippet below.
export const promisify = (fn) => {
return new Promise((resolve, reject) => {
fn
.then(response => resolve(response))
.catch(error => reject(error));
});
};
Add a new file called 'fetch-hook.js' inside hooks folder and add the code snippet below.
import { useCallback, useEffect, useRef } from "react";
import { promisify } from "../utils/commonFunctions";
export const useFetch = () => {
const isUnmounted = useRef(false);
useEffect(() => {
isUnmounted.current = false;
return () => {
isUnmounted.current = true;
};
}, []);
const call = useCallback((fn, onSuccess, onError = null) => {
promisify(fn).then(response => {
console.group('useFetch Hook response', response);
if (!isUnmounted.current) {
console.log('updating state..');
onSuccess(response.data);
}
else
console.log('aborted state update!');
console.groupEnd();
}).catch(error => {
console.log("useFetch Hook error", error);
if (!isUnmounted.current)
if (onError)
onError(error);
});
}, []);
return { call }
};
Folder Structure
Our custom hook is now ready. We use it in our component like below
const OurComponent = (props) => {
//..
const [subscriptions, setSubscriptions] = useState<any>([]);
//..
const { call } = useFetch();
// example method, change with your own
const getSubscriptions = useCallback(async () => {
call(
payment.companySubscriptions(userId), // example api call, change with your own
(data) => setSubscriptions(data),
);
}, [userId]);
//..
const updateSubscriptions = useCallback(async () => {
setTimeout(async () => {
await getSubscriptions();
}, 5000);// 5 seconds delay
}, [getSubscriptions]);
//..
}
In our component, we call 'updateSubscriptions' method. It will trigger 'getSubscriptions' method in which we used our custom hook. If we try to navigate to a different page after calling updateSubscriptions method before 5 seconds over, our custom hook will abort state update and prevent that warning on the title of this question
Wanna see opposite?
Change 'getSubscriptions' method with the one below
const getSubscriptions = useCallback(async () => {
const response = await payment.companySubscriptions(userId);
setSubscriptions(response);
}, [userId]);
Now try to call 'updateSubscriptions' method and navigate to a different page before 5 seconds over
Try this custom hook:
import { useEffect, useRef } from 'react';
export const useIsMounted = () => {
const isMounted = useRef(false);
useEffect(() => {
isMounted.current = true;
return () => (isMounted.current = false);
}, []);
return isMounted;
};
function Example() {
const isMounted = useIsMounted();
const [text, setText] = useState();
const safeSetState = useCallback((callback, ...args) => {
if (isMounted.current) {
callback(...args);
}
}, []);
useEffect(() => {
safeSetState(setText, 'Hello')
});
}, []);
return <h2>{text}</h2>;
}

How to clean up setInterval in useEffect using react hooks

I am trying to create a loading component that will add a period to a div periodically, every 1000ms using setInterval in React. I am trying to cleanup setInterval using the method described in the docs.
https://reactjs.org/docs/hooks-effect.html#example-using-hooks-1
import React, { useEffect, useState } from 'react'
const Loading = () => {
const [loadingStatus, setLoadingStatus] = useState('.')
const [loop, setLoop] = useState()
useEffect(() => {
setLoop(setInterval(() => {
console.log("loading")
setLoadingStatus(loadingStatus + ".")
}, 1000))
return function cleanup() {
console.log('cleaning up')
clearInterval(loop)
}
}, [])
return (<p>
{`Loading ${loadingStatus}`}
</p>)
}
export default Loading
However , the loadingStatus variable only updates once and the setInterval loop doesnt get cleared even after the component stops mounting. Do I have to make this using a class component?
Dependencies are our hint for React of when the effect should run, even though we set an interval and providing no dependencies [], React wont know we want to run it more then once because nothing really changes in our empty dependencies [].
To get the desired result we need to think when we want to run the effect ?
We want to run it when loadingStatus changes, so we need to add loadingStatus as our dependency because we want to run the effect every time loadingStatus changes.
We have 2 options
Add loadingStatus as our dependency.
const Loading = () => {
const [loadingStatus, setLoadingStatus] = useState(".");
const [loop, setLoop] = useState();
useEffect(
() => {
setLoop(
setInterval(() => {
console.log("loading");
setLoadingStatus(loadingStatus + ".");
}, 1000)
);
return function cleanup() {
console.log("cleaning up");
clearInterval(loop);
};
},
[loadingStatus]
);
return <p>{`Loading ${loadingStatus}`}</p>;
};
Make our effect not aware that we use loadingStatus
const Loading = () => {
const [loadingStatus, setLoadingStatus] = useState(".");
useEffect(() => {
const intervalId = setInterval(() => {
setLoadingStatus(ls => ls + ".");
}, 1000);
return () => clearInterval(intervalId);
}, []);
return <p>{`Loading ${loadingStatus}`}</p>;
};
Read more here => a-complete-guide-to-useeffect

Categories