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.
Related
I have a button component that has a button inside that has a state passed to it isActive and a click function. When the button is clicked, the isActive flag will change and depending on that, the app will fetch some data. The button's parent component does not rerender. I have searched on how to force stop rerendering for a component and found that React.memo(YourComponent) must do the job but still does not work in my case. It also make sense to pass a check function for the memo function whether to rerender or not which I would set to false all the time but I cannot pass another argument to the function. Help.
button.tsx
interface Props {
isActive: boolean;
onClick: () => void;
}
const StatsButton: React.FC<Props> = ({ isActive, onClick }) => {
useEffect(() => {
console.log('RERENDER');
}, []);
return (
<S.Button onClick={onClick} isActive={isActive}>
{isActive ? 'Daily stats' : 'All time stats'}
</S.Button>
);
};
export default React.memo(StatsButton);
parent.tsx
const DashboardPage: React.FC = () => {
const {
fetchDailyData,
fetchAllTimeData,
} = useDashboard();
useEffect(() => {
fetchCountry();
fetchAllTimeData();
// eslint-disable-next-line
}, []);
const handleClick = useEventCallback(() => {
if (!statsButtonActive) {
fetchDailyData();
} else {
fetchAllTimeData();
}
setStatsButtonActive(!statsButtonActive);
});
return (
<S.Container>
<S.Header>
<StatsButton
onClick={handleClick}
isActive={statsButtonActive}
/>
</S.Header>
</S.Container>
)
}
fetch functions are using useCallback
export const useDashboard = (): Readonly<DashboardOperators> => {
const dispatch: any = useDispatch();
const fetchAllTimeData = useCallback(() => {
return dispatch(fetchAllTimeDataAction());
}, [dispatch]);
const fetchDailyData = useCallback(() => {
return dispatch(fetchDailyDataAction());
}, [dispatch]);
return {
fetchAllTimeData,
fetchDailyData,
} as const;
};
You haven't posted all of parent.tsx, but I assume that handleClick is created within the body of the parent component. Because the identity of the function will be different on each rendering of the parent, that causes useMemo to see the props as having changed, so it will be re-rendered.
Depending on if what's referenced in that function is static, you may be able to use useCallback to pass the same function reference to the component on each render.
Note that there is an RFC for something even better than useCallback; if useCallback doesn't work for you look at how useEvent is defined for an idea of how to make a better static function reference. It looks like that was even published as a new use-event-callback package.
Update:
It sounds like useCallback won't work for you, presumably because the referenced variables used by the callback change on each render, causing useCallback to return different values, thus making the prop different and busting the cache used by useMemo. Try that useEventCallback approach. Just to illustrate how it all works, here's a naive implementation.
function useEventCallback(fn) {
const realFn = useRef(fn);
useEffect(() => {
realFn.current = fn;
}, [fn]);
return useMemo((...args) => {
realFn.current(...args)
}, []);
}
This useEventCallback always returns the same memoized function, so you'll pass the same value to your props and not cause a re-render. However, when the function is called it calls the version of the function passed into useEventCallback instead. You'd use it like this in your parent component:
const handleClick = useEventCallback(() => {
if (!statsButtonActive) {
fetchDailyData();
} else {
fetchAllTimeData();
}
setStatsButtonActive(!statsButtonActive);
});
I am trying to implement an onkeydown event handler into my react app to close the settings screen by pressing ESC:
useEffect(() => {
document.addEventListener("keydown", handleKeyPress, false);
return () => {
document.removeEventListener("keydown", handleKeyPress, false);
};
}, [])
const saveSettings = () => {
window.localStorage.setItem("storage.ls.version", CURRENT_LOCALSTORAGE_SCHEMA_VERSION);
window.localStorage.setItem("auth.token", authInput);
window.localStorage.setItem("filter.class", classInput);
window.localStorage.setItem(
"filter.subjects",
JSON.stringify(subjectsInput.filter((v: string) => v.trim() !== ""))
);
props.dismiss();
};
const handleKeyPress = (event: KeyboardEvent) => {
if (event.key === "Escape") {
saveSettings();
}
};
The callback does execute, props.dismiss() (in saveSettings) runs just fine. But the changes don't seem to be saved to localStorage. However, when I use the same saveSettings function on a press of a button (which I have been doing already before), it works and saves as expected.
I wasn't able to find something, but is there a restriction that prevents usage of localStorage in event callbacks? Or is there another reason it doesn't work as expected?
See the comment thread under the question; here's the code that reads the localStorage:
const [authInput, setAuthInput] = useState(window.localStorage.getItem("auth.token") ?? "");
const [classInput, setClassInput] = useState(window.localStorage.getItem("filter.class") ?? "");
const [subjectsInput, setSubjectsInput] = useState(
JSON.parse(window.localStorage.getItem("filter.subjects") ?? "[]")
);
Couple of issues here that are contributing to your issue:
The handleKeyPress function is bound as the event handler for the keydown event in the first useEffect, and so it holds the context at that point in time when it runs. So say you have values in local storage already like authInput = apple, classInput = english and subjectsInput = '', and you change the value to authInput = banana, then you hit ESC, the value that will be stored in authInput will be apple again because that was the initial value of authInput when you attached handleKeyPress to the keydown event in the DOM, and will always be apple! So it 'looks' as if the value isn't changing no matter what you do. To fix this, you need to add handleKeyPress as a dependency of that first useEffect(). This function context changes with every state change, because the saveSettings() function has different values for authInput, classInput and all the other values it's referencing, whenever the state changes. So the first change you need to make is the following:
useEffect(() => {
document.addEventListener("keydown", handleKeyPress, false);
return () => {
document.removeEventListener("keydown", handleKeyPress, false);
};
}, [handleKeyPress]); // add the function as a dependency
handleKeyPress doesn't need to change if the values / functions it's referencing inside doesn't change. So use React's useCallback() to specify this relationship:
const handleKeyPress = useCallback((event) => {
if (event.key === "Escape") {
saveSettings();
}
}, [saveSettings]);
See https://reactjs.org/docs/hooks-reference.html#usecallback for more on this.
Lastly, apply the same treatment to saveSettings() because that function depends on three values:
const saveSettings = useCallback(() => {
window.localStorage.setItem("storage.ls.version", CURRENT_LOCALSTORAGE_SCHEMA_VERSION);
window.localStorage.setItem("auth.token", authInput);
window.localStorage.setItem("filter.class", classInput);
window.localStorage.setItem(
"filter.subjects",
JSON.stringify(subjectsInput.filter((v: string) => v.trim() !== ""))
);
props.dismiss();
}, [authInput, classInput, subjectsInput]);
Whenever the state of authInput, classInput or subjectsInput changes, React will run the cleanup that you specified in the useEffect() and re-run the effect with the new handler that has the correct state values in its run-time context.
If you still don't understand why this is happening, that's ok! Don't fret - I highly encourage you to read how Closures work in Javascript: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures or https://javascript.info/closure
Lastly, I want to mention some code smells for you that you might want to cleanup:
When setting the initial states for all of your state variables, wrap the expensive operation of reading from localStorage in a function so React only runs it once, instead of every re-render.
const [authInput, setAuthInput] = useState(window.localStorage.getItem("auth.token") ?? "");
// instead do this
const [authInput, setAuthInput] = useState(() => window.localStorage.getItem("auth.token") ?? "");
Is subjectsInput guaranteed to be an array when you run filter() on it in saveSettings()? You might want to account for this.
I have a react hook that might help you.
useKeyboardEvents.ts
import { useEffect } from "react";
export type UseKeyboardEventsProps = {
[key in keyof WindowEventMap]?: EventListenerOrEventListenerObject;
};
export const useKeyboardEvents = (value: UseKeyboardEventsProps) => {
useEffect(() => {
Object.entries(value).forEach((item) => {
const [key, funct] = item;
window.addEventListener(
key,
funct as EventListenerOrEventListenerObject,
false
);
});
return () => {
Object.entries(value).forEach((item) => {
const [key, funct] = item;
window.removeEventListener(
key,
funct as EventListenerOrEventListenerObject,
false
);
});
};
}, [value]);
};
Use it like follows. Note: I used keydown like in your code, but I think it would be better to listen to keyup. (replace your useEffect with the following)
useKeyboardEvents({
keydown: (event: any) => {
if (event.key === 'Escape') {
saveSettings();
}
}
});
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>
);
}
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])
}
I'm playing around with React Native and lodash's debounce.
Using the following code only make it work like a delay and not a debounce.
<Input
onChangeText={(text) => {
_.debounce(()=> console.log("debouncing"), 2000)()
}
/>
I want the console to log debounce only once if I enter an input like "foo". Right now it logs "debounce" 3 times.
Debounce function should be defined somewhere outside of render method since it has to refer to the same instance of the function every time you call it as oppose to creating a new instance like it's happening now when you put it in the onChangeText handler function.
The most common place to define a debounce function is right on the component's object. Here's an example:
class MyComponent extends React.Component {
constructor() {
this.onChangeTextDelayed = _.debounce(this.onChangeText, 2000);
}
onChangeText(text) {
console.log("debouncing");
}
render() {
return <Input onChangeText={this.onChangeTextDelayed} />
}
}
2019: Use the 'useCallback' react hook
After trying many different approaches, I found using 'useCallback' to be the simplest and most efficient at solving the multiple calls problem.
As per the Hooks API documentation, "useCallback returns a memorized version of the callback that only changes if one of the dependencies has changed."
Passing an empty array as a dependency makes sure the callback is called only once. Here's a simple implementation.
import React, { useCallback } from "react";
import { debounce } from "lodash";
const handler = useCallback(debounce(someFunction, 2000), []);
const onChange = (event) => {
// perform any event related action here
handler();
};
Hope this helps!
Updated 2021
As other answers already stated, the debounce function reference must be created once and by calling the same reference to denounce the relevant function (i.e. changeTextDebounced in my example).
First things first import
import {debounce} from 'lodash';
For Class Component
class SomeClassComponent extends React.Component {
componentDidMount = () => {
this.changeTextDebouncer = debounce(this.changeTextDebounced, 500);
}
changeTextDebounced = (text) => {
console.log("debounced");
}
render = () => {
return <Input onChangeText={this.changeTextDebouncer} />;
}
}
For Functional Component
const SomeFnComponent = () => {
const changeTextDebouncer = useCallback(debounce(changeTextDebounced, 500), []);
const changeTextDebounced = (text) => {
console.log("debounced");
}
return <Input onChangeText={changeTextDebouncer} />;
}
so i came across the same problem for textInput where my regex was being called too many times did below to avoid
const emailReg = /^\w+([\.-]?\w+)*#\w+([\.-]?\w+)*(\.\w\w+)+$/;
const debounceReg = useCallback(debounce((text: string) => {
if (emailReg.test(text)) {
setIsValidEmail(true);
} else {
setIsValidEmail(false);
}
}, 800), []);
const onChangeHandler = (text: string) => {
setEmailAddress(text);
debounceReg(text)
};
and my debounce code in utils is
function debounce<Params extends any[]>(
f: (...args: Params) => any,
delay: number,
): (...args: Params) => void {
let timer: NodeJS.Timeout;
return (...args: Params) => {
clearTimeout(timer);
timer = setTimeout(() => {
f(...args);
}, delay);
};
}