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);
};
}
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'm learning React and have a custom Context accessible with a use method:
// In a Provider file.
export const useUsefulObject(): UsefulObject {
return useContext(...)
}
I want to use UsefulObject in a click callback in another file. For convenience, I have that callback wrapped in a method someLogic.
const PageComponent: React.FC = () => {
return <Button onClick={(e) => someLogic(e)} />;
}
function someLogic(e: Event) {
const usefulObject = useUsefulObject();
usefulObject.foo(e, ...);
}
However, VS Code alerts that calling useUsefulObject in someLogic is a violation of the rules of hooks since it's called outside of a component method.
I can pass the object down (the below code works!) but it feels like it defeats the point of Contexts to avoid all the passing down. Is there a better way than this?
const PageComponent: React.FC = () => {
const usefulObject = useUsefulObject();
return <Button onClick={(e) => someLogic(e, usefulObject)} />;
}
function someLogic(e: Event, usefulObject) {
usefulObject.foo(e, ...);
}
The hooks need to be called while the component is rendering, so your last idea is one of the possible ways to do so. Another option is you can create a custom hook which accesses the context and creates the someLogic function:
const PageComponent: React.FC = () => {
const someLogic = useSomeLogic();
return <Button onClick={(e) => someLogic(e)} />
}
function useSomeLogic() {
const usefulObject = useUsefulObject();
const someLogic = useCallback((e: Event) => {
usefulObject.foo(e, ...);
}, [usefulObject]);
return someLogic;
}
I'm trying to challenge myself to convert my course project that uses hooks into the same project but without having to use hooks in order to learn more about how to do things with class components. Currently, I need help figuring out how to replicate the useCallback hook within a normal class component. Here is how it is used in the app.
export const useMovieFetch = movieId => {
const [state, setState] = useState({});
const [loading, setLoading] = useState(true);
const [error, setError] = useState(false);
const fetchData = useCallback(async () => {
setError(false);
setLoading(true);
try{
const endpoint = `${API_URL}movie/${movieId}?api_key=${API_KEY}`;
const result = await(await fetch(endpoint)).json();
const creditsEndpoint = `${API_URL}movie/${movieId}/credits?api_key=${API_KEY}`;
const creditsResult = await (await fetch(creditsEndpoint)).json();
const directors = creditsResult.crew.filter(member => member.job === 'Director');
setState({
...result,
actors: creditsResult.cast,
directors
});
}catch(error){
setError(true);
console.log(error);
}
setLoading(false);
}, [movieId])
useEffect(() => {
if(localStorage[movieId]){
// console.log("grabbing from localStorage");
setState(JSON.parse(localStorage[movieId]));
setLoading(false);
}else{
// console.log("Grabbing from API");
fetchData();
}
}, [fetchData, movieId])
useEffect(() => {
localStorage.setItem(movieId, JSON.stringify(state));
}, [movieId, state])
return [state, loading, error]
}
I understand how to replicate other hooks such as useState and useEffect but I'm struggling to find the answer for the alternative to useCallback. Thank you for any effort put into this question.
TL;DR
In your specific example useCallback is used to generate a referentially-maintained property to pass along to another component as a prop. You do that by just creating a bound method (you don't have to worry about dependencies like you do with hooks, because all the dependencies are maintained on your instance as props or state.
class Movie extends Component {
constructor() {
this.state = {
loading:true,
error:false,
}
}
fetchMovie() {
this.setState({error:false,loading:true});
try {
// await fetch
this.setState({
...
})
} catch(error) {
this.setState({error});
}
}
fetchMovieProp = this.fetchMovie.bind(this); //<- this line is essentially "useCallback" for a class component
render() {
return <SomeOtherComponent fetchMovie={this.fetchMovieProp}/>
}
}
A bit more about hooks on functional vs class components
The beautiful thing about useCallback is, to implement it on a class component, just declare an instance property that is a function (bound to the instance) and you're done.
The purpose of useCallback is referential integrity so, basically, your React.memo's and React.PureComponent's will work properly.
const MyComponent = () => {
const myCallback = () => { ... do something };
return <SomeOtherComponent myCallback={myCallback}/> // every time `MyComponent` renders it will pass a new prop called `myCallback` to `SomeOtherComponent`
}
const MyComponent = () => {
const myCallback = useCallback(() => { ... do something },[...dependencies]);
return <SomeOtherComponent myCallback={myCallback}/> // every time `MyComponent` renders it will pass THE SAME callback to `SomeOtherComponent` UNLESS one of the dependencies changed
}
To replicate useCallback in class components you don't have to do anything:
class MyComponent extends Component {
method() { ... do something }
myCallback = this.method.bind(this); <- this is essentially `useCallback`
render() {
return <SomeOtherComponent myCallback={this.myCallback}/> // same referential integrity as `useCallback`
}
}
THE BIG ONE LINER
You'll find that hooks in react are just a mechanism to create instance variables (hint: the "instance" is a Fiber) when all you have is a function.
You can replicate the behavior ofuseCallback by using a memorized function for the given input(eg: movieId)
You can use lodash method
for more in-depth understanding check here
I'm trying to learn Jest and Enzyme but I'm having a problem I can't find a solution to, this is my test.. it's not very good I know but I'm learning:
import * as apiMock from '../api';
const fakePostId = '1';
const fakePersona = 'Fake';
jest.mock('../api', () => {
return {
fetchAllComments: jest.fn(() => {
return [];
}),
filterComments: jest.fn(() => {
return [];
}),
createCommentObject: jest.fn(() => {
return [];
}),
};
});
test('checks if functions are called after didMount', () => {
const component = shallow(
<Comments postId={fakePostId} currentPersona={fakePersona} />
);
const spySetComments = jest.spyOn(
component.instance(),
'setCommentsFromLocalStorage'
);
component.instance().componentDidMount();
expect(spySetComments).toHaveBeenCalledTimes(1);
//Don't know why these are called 2! times, I can't see why removing componentDidMount makes it 0.
expect(apiMock.fetchAllComments).toHaveBeenCalledTimes(1);
expect(apiMock.filterComments).toHaveBeenCalledTimes(1);
}
The problem is toHaveBeenCalledTimes(1) fails with reason:
Expected mock function to have been called one time, but it was called
two times.
But I don't know why.
setCommentsFromLocalStorage only runs once, and that is the function that sohuld run from componentDidMount and execute these api calls once.
ReactComponent looks like this:
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import CreateNewComment from './CreateNewComment';
import SingleComment from './SingleComment';
import * as api from '../api';
class Comments extends Component {
state = {
comments: []
};
componentDidMount() {
this.setCommentsFromLocalStorage();
}
setCommentsFromLocalStorage = (postId = this.props.postId) => {
const fetchedComments = api.fetchAllComments();
const comments = api.filterComments(fetchedComments, postId);
this.setState({ comments });
};
removeComment = commentId => {
api.removeComment(commentId);
this.setCommentsFromLocalStorage();
};
renderCommentList = (comments, currentPersona) =>
comments.map(comment => (
<SingleComment
{...comment}
currentPersona={currentPersona}
key={comment.id}
onClick={this.removeComment}
/>
));
render() {
return (
<div className="py-2">
<h2 className="text-indigo-darker border-b mb-4">Comments</h2>
{this.renderCommentList(this.state.comments, this.props.currentPersona)}
<CreateNewComment
postId={this.props.postId}
author={this.props.currentPersona}
updateComments={this.setCommentsFromLocalStorage}
/>
</div>
);
}
}
Comments.propTypes = {
postId: PropTypes.string.isRequired,
currentPersona: PropTypes.string.isRequired
};
export default Comments;
componentDidMount gets called during shallow().
This means that setCommentsFromLocalStorage gets called which calls fetchAllComments and filterComments all during that initial shallow() call.
api has already been mocked so it records those calls to fetchAllComments and filterComments.
Once that has all happened, the spy is created for setCommentsFromLocalStorage and componentDidMount gets called again (which calls fetchAllComments and filterComments again).
The spy for setCommentsFromLocalStorage then correctly reports that it was called once (since it only existed during the second call to componentDidMount).
The spies on fetchAllComments and filterComments then correctly report that they were called two times since they existed during both calls to componentDidMount.
The easiest way to fix the test is to clear the mocks on fetchAllComments and filterComments before the call to componentDidMount:
apiMock.fetchAllComments.mockClear(); // clear the mock
apiMock.filterComments.mockClear(); // clear the mock
component.instance().componentDidMount();
expect(spySetComments).toHaveBeenCalledTimes(1); // SUCCESS
expect(apiMock.fetchAllComments).toHaveBeenCalledTimes(1); // SUCCESS
expect(apiMock.filterComments).toHaveBeenCalledTimes(1); // SUCCESS
Use beforeEach and afterEach to mock and mockRestore the spies, respectively.
This is explained in the Setup and Teardown section in Jest docs.
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.