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])
}
Related
I have created some state at the top level component (App), however it seems that when this state is updated, the updated state is not read by the asynchronous function defined in useEffect() (it still uses the previous value), more detail below:
I am attempting to retrieve the state of the const processing in the async function toggleProcessing defined in useEffect(), so that when processing becomes false, the async function exits from the while loop. However, it seems that when the processing updates to false, the while loop still keeps executing.
The behaviour should be as follows: Pressing the 'Begin Processing' button should console log "Processing..." every two seconds, and when that same button is pressed again (now labeled 'Stop Processing'), then "Stopping Processing" should be console logged. However, in practice, "Stopping Processing" is never console logged, and "Processing" is continuously logged forever.
Below is the code:
import React, { useState, useEffect} from 'react'
const App = () => {
const [processing, setProcessing] = useState(false)
const sleep = (ms) => {
return new Promise(resolve => setTimeout(resolve, ms))
}
useEffect(() => {
const toggleProcessing = async () => {
while (processing) {
console.log('Processing...')
await sleep(2000);
}
console.log('Stopping Processing')
}
if (processing) {
toggleProcessing() // async function
}
}, [processing])
return (
<>
<button onClick={() => setProcessing(current => !current)}>{processing ? 'Stop Processing' : 'Begin Processing'}</button>
</>
)
}
export default App;
It really just comes down to being able to read the updated state of processing in the async function, but I have not figure out a way to do this, despite reading similar posts.
Thank you in advance!
If you wish to access a state when using timeouts, it's best to keep a reference to that variable. You can achieve this using the useRef hook. Simply add a ref with the processing value and remember to update it.
const [processing, setProcessing] = useState<boolean>(false);
const processingRef = useRef(null);
useEffect(() => {
processingRef.current = processing;
}, [processing]);
Here is the working code:
import React, { useState, useEffect, useRef} from 'react'
const App = () => {
const [processing, setProcessing] = useState(false)
const processingRef = useRef(null);
const sleep = (ms) => {
return new Promise(resolve => setTimeout(resolve, ms))
}
useEffect(() => {
const toggleProcessing = async () => {
while (processingRef.current) {
console.log('Processing')
await sleep(2000);
}
console.log('Stopping Processing')
}
processingRef.current = processing;
if (processing) {
toggleProcessing() // async function
}
}, [processing])
return (
<>
<button onClick={() => setProcessing(current => !current)}>{processing ? 'Stop Processing' : 'Begin Processing'}</button>
</>
)
}
export default App;
I was interested in how this works and exactly what your final solution was based on the accepted answer. I threw together a solution based on Dan Abramov's useInterval hook and figured this along with a link to some related resources might be useful to others.
I'm curious, is there any specific reason you decided to use setTimeout and introduce async/await and while loop rather than use setInterval? I wonder the implications. Will you handle clearTimeout on clean up in the effect if a timer is still running on unmount? What did your final solution look like?
Demo/Solution with useInterval
https://codesandbox.io/s/useinterval-example-processing-ik61ho
import React, { useState, useEffect, useRef } from "react";
const App = () => {
const [processing, setProcessing] = useState(false);
useInterval(() => console.log("processing"), 2000, processing);
return (
<div>
<button onClick={() => setProcessing((prev) => !prev)}>
{processing ? "Stop Processing" : "Begin Processing"}
</button>
</div>
);
};
function useInterval(callback, delay, processing) {
const callbackRef = useRef();
// Remember the latest callback.
useEffect(() => {
callbackRef.current = callback;
}, [callback]);
// Set up the interval.
useEffect(() => {
function tick() {
callbackRef.current();
}
if (delay !== null && processing) {
let id = setInterval(tick, delay);
console.log(`begin processing and timer with ID ${id} running...`);
// Clear timer on clean up.
return () => {
console.log(`clearing timer with ID ${id}`);
console.log("stopped");
clearInterval(id);
};
}
}, [delay, processing]);
}
export default App;
Relevant Links
Dan Abramov - Making setInterval Declarative with React Hooks
SO Question: React hooks - right way to clear timeouts and intervals
setTimeout and clearTimeout in React with Hooks (avoiding memory leaks by clearing timers on clean up in effects)
Let's say I have a simple controlled input component in React.
const ControlledInput = () => {
const [state, setState] = React.useState("");
const handleInputChange = (e) => {
setState(e.target.value);
};
return <input type="text" value={state} onChange={handleInputChange} />
};
So, in this simple example, the component will be re-rendered with each character user entered. What is the best experience used to delay re-render? I mean user types to the input the word " I am user " and we want that re-render to happen when the user stops entering characters or maybe after 3 seconds when the user started to type?
I use SetTimeout to delay a re-render, here is code example
import React, { useState, useEffect } from 'react';
type Props = {
children: React.ReactElement;
waitBeforeShow?: number;
};
const Delayed = ({ children, waitBeforeShow = 500 }: Props) => {
const [isShown, setIsShown] = useState(false);
useEffect(() => {
console.log(waitBeforeShow);
setTimeout(() => {
setIsShown(true);
}, waitBeforeShow);
}, [waitBeforeShow]);
return isShown ? children : null;
};
export default Delayed;
And
export function LoadingScreen = ({ children }: Props) => {
return (
<Delayed>
<div />
</Delayed>
);
};
The best way, in this case, would be using the setTimeout() method for adding some delay to the rendering of component or API calls.
Note: In the below-shared example you should not use the inputValue var which I control using some delay it's sole purpose is to debounce the typing.
This is what you will have to use setTimeout() in a way that it gets cleared every time the user inputs something and for that, you will need to save setTimeout() reference using bellow snippet
var timoutId = setTimeout(cb,timeout);
After this, you can clear the timeout by calling this clearTimeout(timeoutId)
I have created a short demo which worth checking out: Sandbox demo
Here is the final snippet which is used in the above mentioned sandbox:
import React, { useState, useCallback } from "react";
import "./styles.css";
export default function App() {
// for preserving input value with delay
const [inputValue, setValue] = useState("");
// to maintain the timeout ref
let intervalId = React.useRef();
// handle input change with delay
const handleChange = useCallback((e) => {
// input box value
const value = e.target.value;
console.log(value, "value");
// clear the existing timout
clearTimeout(intervalId.current);
// reassign new timout ref
intervalId.current = setTimeout(() => {
setValue(value);
console.log("I'm inside the setTimeout callback function!");
}, 3000);
}, []);
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<input onChange={handleChange} />
<p>{inputValue}</p>
</div>
);
}
Editor component
import Editor from '#monaco-editor/react';
import { useDebounce } from './useDebounce';
import { useEffect, useState } from 'react';
type Props = {
code: string;
onChange: (code: string) => void;
disabled?: boolean;
};
export const GraphqlCodeEditor = ({
onChange,
code,
disabled = false,
}: Props) => {
const [editorValue, setEditorValue] = useState(code);
const editorValueDebounced = useDebounce(editorValue, 500);
useEffect(() => {
onChange(editorValueDebounced);
}, [editorValueDebounced, onChange]);
useEffect(() => {
if (code !== editorValueDebounced) {
setEditorValue(code);
}
}, [code, editorValueDebounced]);
return (
<Editor
options={{
minimap: { enabled: false },
autoClosingBrackets: 'always',
readOnly: disabled,
}}
language="graphql"
value={editorValue}
onChange={(value) => {
if (value) {
setEditorValue(value);
}
}}
/>
);
};
useDebounce hook
import { useEffect, useState } from 'react';
export const useDebounce = <T>(value: T, delay: number) => {
// State and setters for debounced value
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(
() => {
// Update debounced value after delay
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
// Cancel the timeout if value changes (also on delay change or unmount)
// This is how we prevent debounced value from updating if value is changed ...
// .. within the delay period. Timeout gets cleared and restarted.
return () => {
clearTimeout(handler);
};
},
[value, delay] // Only re-call effect if value or delay changes
);
return debouncedValue;
};
The code prop in the editor component is managed by a parent component. When e.g a user loads a new code snippet it's updated and the editor should load the new value. The editor should also debounce it's value so the onChange() function isn't called on every keypress.
The editor component above results in a loop where the component switches between the previous value and the new every 500ms.
How can I achieve this with the useDebounce hook?
This is closely related to this post I think, however it's using lodash debounce. I was hoping to achieve the same with the useDebounce hook.
The problem was this useEffect()
useEffect(() => {
if (code !== editorValueDebounced) {
setEditorValue(code);
}
}, [code, editorValueDebounced]);
Changing it to this fixed the problem
useEffect(() => {
setEditorValue(code);
}, [code]);
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);
I have a list of elements, when hovering one of these, I'd like to change my state.
<ListElement onMouseOver={() => this.setState({data})}>Data</ListElement>
Unfortunately, if I move my mouse over the list, my state changes several times in a quick succession. I'd like to delay the change on state, so that it waits like half a second before being fired. Is there a way to do so?
Here's a way you can delay your event by 500ms using a combination of onMouseEnter, onMouseLeave, and setTimeout.
Keep in mind the state update for your data could be managed by a parent component and passed in as a prop.
import React, { useState } from 'react'
const ListElement = () => {
const [data, setData] = useState(null)
const [delayHandler, setDelayHandler] = useState(null)
const handleMouseEnter = event => {
setDelayHandler(setTimeout(() => {
const yourData = // whatever your data is
setData(yourData)
}, 500))
}
const handleMouseLeave = () => {
clearTimeout(delayHandler)
}
return (
<div
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
I have a delayed event handler
</div>
)
}
export default ListElement
I might be a little late for this but I'd like to add to some of the answers above using Lodash debounce. When debouncing a method, the debounce function lets you cancel the call to your method based on some event. See example for functional component:
const [isHovered, setIsHovered] = React.useState(false)
const debouncedHandleMouseEnter = debounce(() => setIsHovered(true), 500)
const handlOnMouseLeave = () => {
setIsHovered(false)
debouncedHandleMouseEnter.cancel()
}
return (
<div
onMouseEnter={debouncedHandleMouseEnter}
onMouseLeave={handlOnMouseLeave}
>
... do something with isHovered state...
</div>
)
This example lets you call your function only once the user is hovering in your element for 500ms, if the mouse leaves the element the call is canceled.
You can use debounce as a dedicated package or get it from lodash, etc:
Useful for implementing behavior that should only happen after a repeated action has completed.
const debounce = require('debounce');
class YourComponent extends Component {
constructor(props) {
super(props);
this.debouncedMouseOver = debounce(handleMouseOver, 200);
}
handleMouseOver = data => this.setState({ data });
render() {
const data = [];
return <ListElement onMouseOver={() => this.debouncedMouseOver(data)}>Data</ListElement>;
}
}
You can create a method that will trigger the onMouseOver event when matching special requirements.
In the further example, it triggers after 500 ms.
/**
* Hold the descriptor to the setTimeout
*/
protected timeoutOnMouseOver = false;
/**
* Method which is going to trigger the onMouseOver only once in Xms
*/
protected mouseOverTreatment(data) {
// If they were already a programmed setTimeout
// stop it, and run a new one
if (this.timeoutOnMouseOver) {
clearTimeout(this.timeoutOnMouseOver);
}
this.timeoutOnMouseOver = setTimeout(() => {
this.setState(data);
this.timeoutOnMouseOver = false;
}, 500);
}
debounce is always the answer if you want to limit the action in a time frame.
Implementation is simple, no need for external libraries.
implementation:
type Fnc = (...args: any[]) => void;
// default 300ms delay
export function debounce<F extends Fnc>(func: F, delay = 300) {
type Args = F extends (...args: infer P) => void ? P : never;
let timeout: any;
return function (this: any, ...args: Args) {
clearTimeout(timeout);
timeout = setTimeout(() => {
func.apply(this, args);
}, delay);
};
}
usage:
...
/** any action you want to debounce */
function foo(
data: any,
event: React.MouseEvent<HTMLDivElement, MouseEvent>
): void {
this.setState({data});
}
const fooDebounced = debounce(foo, 500);
<ListElement onMouseOver={fooDebounced.bind(null, data)}>
Data
</ListElement>
...
You don't actually have to bind a function, but it's a good habit if you loop through multiple elements to avoid initializing a new function for each element.