Every function called in `useEffect` stack must be wrapped in `useCallback`? - reactjs

I am new to React and it seems to me that if you use a function inside of useEffect, that entire stack has to be wrapped in useCallback in order to comply with the linter.
For example:
const Foo = ({} => {
const someRef = useRef(0);
useEffect(() => {
startProcessWithRef();
}, [startProcessWithRef]);
const handleProcessWithRef = useCallback((event) => {
someRef.current = event.clientY;
}, []);
const startProcessWithRef = useCallback(() => {
window.addEventListener('mousemove', handleProcessWithRef);
}, [handleProcessWithRef]);
...
});
I'm wondering if there is a different pattern where I don't have to make the entire chain starting in useEffect calling startProcessWithRef be wrapped in useCallback with dependencies. I am not saying it is good or bad, I'm just seeing if there is a preferred alternative because I am new and don't know of one.

The idiomatic way to write your example would be similar to this:
Note the importance of removing the event listener in the effect cleanup function.
TS Playground
import {useEffect, useRef} from 'react';
const Example = () => {
const someRef = useRef(0);
useEffect(() => {
const handleMouseMove = (event) => { someRef.current = event.clientY; };
window.addEventListener('mousemove', handleMouseMove);
return () => window.removeEventListener('mousemove', handleMouseMove);
}, [someRef]);
return null;
};
If you prefer defining the function outside the effect hook, you'll need useCallback:
import {useCallback, useEffect, useRef} from 'react';
const Example = () => {
const someRef = useRef(0);
const updateRefOnMouseMove = useCallback(() => {
const handleMouseMove = (event) => { someRef.current = event.clientY; };
window.addEventListener('mousemove', handleMouseMove);
return () => window.removeEventListener('mousemove', handleMouseMove);
}, [someRef]);
useEffect(updateRefOnMouseMove, [updateRefOnMouseMove]);
return null;
};

Related

How to test window.addEventListener('scroll') in React Testing Library?

I have this hook in React I want to write the unit test but I faced the problem that I don't know how I could cover handleScroll function, how can I go to the useEffect to trigger the scroll event?
I tried fireEvent.scroll but not success.
import { useState, useEffect } from 'react';
const scrollWindow = (
fetchOffset: number,
callback: () => void,
preventFetch?: boolean,
) => {
const [isFetching, setIsFetching] = useState(false);
const handleScroll = () => {
const scrollFromBottom = docElem.scrollHeight - docElem.scrollTop - docElem.clientHeight;
if (scrollFromBottom - fetchOffset > 0) return;
setIsFetching(true);
};
useEffect(() => {
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, [isFetching, preventFetch]);
useEffect(() => {
if (!isFetching) return;
callback();
}, [isFetching]);
return [isFetching, setIsFetching];
};
export default scrollWindow;

Scroll changes state React

Added a change in the color of the appbar when scrolling, but the problem is that the useEffect changes the data array. Every time the color of the appbar changes, the array itself changes.
Can it be rewritten in some other way?
const colorChange = useCallback(() => {
if (window.scrollY >= 200) {
setColor(true)
} else {
setColor(false)
}
}, [])
useEffect(() => {
window.addEventListener('scroll', colorChange)
return () => window.removeEventListener('scroll', colorChange)
}, [colorChange])
The function I use for random arrays
import _ from 'lodash'
export const shuffle = (array) => {
const random = _.shuffle(array)
return random.slice(0, 2)
}
I found the answer. With ref data no longer changes due to scrolling.
I am attaching the solution:
import React, {useEffect, useRef, useState} from 'react'
const ref = useRef()
const [color, setColor] = useState(false)
ref.current = color
useEffect(() => {
const colorChange = () => {
const show = window.scrollY >= 200
if (ref.current !== show) {
setColor(true)
}
}
window.addEventListener('scroll', colorChange)
return () => window.removeEventListener('scroll', colorChange)
}, [])
You don't have to pass colorChange in dependencies of the useEffect. no need for the useCallback. you can define a function inside the useEffect only.
useEffect(() => {
const colorChange = () => setColor(window.scrollY >= 200);
window.addEventListener("scroll", colorChange);
return () => window.removeEventListener("scroll", colorChange);
}, []);

How to throttle often re-rendered component in react

I want to throttle rendering component which connects to WS and often gets data, which cause its very often re-render.
There is my solution with useMemo hook but I'm not sure that useMemo is designed for such things.
For sure every update of data will cause re-render because is that how useState works, and I have to update this data state.
Do you have maybe some advices or ideas how to throttle re-renders of <DataVisualizator /> Component?
useInterval hook
import { useEffect, useRef } from "react";
export const useInterval = (callback: () => void, delay: number) => {
const savedCallback = useRef<() => void>();
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
useEffect(() => {
function tick() {
if (savedCallback.current) {
savedCallback.current();
}
}
if (delay !== null) {
let id = setInterval(tick, delay);
return () => clearInterval(id);
}
}, [delay]);
};
And component which receive data and should throttle his children
import { useEffect, useMemo, useState } from "react";
import useWebSocket from "react-use-websocket";
import { useInterval } from "../Hooks";
export const WebSockets = () => {
const SOCKET_URL = "wss://someWS";
//data will be kind of Dictionary .eg { "key1": val, "anotherkey: valOther }
const [data, setData] = useState({});
const webSocketOptions = {
shouldReconnect: () => true,
retryOnError: true,
reconnectInterval: 3000,
reconnectAttempts: 5,
onError: (e) => console.log(e),
};
const { sendMessage, lastMessage } = useWebSocket(
SOCKET_URL,
webSocketOptions
);
const handleData = (message: RequestData, data: OrderBookData) => {
// lot of operations to deepClone state and set new with new Data
setData(clonedData);
};
useEffect(() => {
lastMessage && handleData(JSON.parse(lastMessage.data), data);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [lastMessage]);
const [tickThrottle, setTickThrottle] = useState(false);
useInterval(() => {
setTickThrottle(!tickThrottle);
}, 700);
//Throttling with useMemo hook
const throttledDataVisdsualizator = useMemo(
() => <DataVisualizator dataToVisualize={data} />,
// eslint-disable-next-line react-hooks/exhaustive-deps
[tickThrottle]
);
return (
<>
{throttledDataVisdsualizator}
</>
);
};
Solution with useMemo hook
const [tickThrottle, setTickThrottle] = useState(false);
useInterval(() => {
setTickThrottle(!tickThrottle);
}, 700);
//Throttling with useMemo hook
const throttledDataVisdsualizator = useMemo(
() => <DataVisualizator dataToVisualize={data} />,
// eslint-disable-next-line react-hooks/exhaustive-deps
[tickThrottle]
);
return (
<>
{throttledDataVisdsualizator}
</>
);

React nested Hooks with state

An extension of a question I was just answered but:
if you want to nest hooks that call state, is there a sane way of doing it, of should you just not do it?
For example:
const {useEffect, useState, useRef} = React;
const someSubHook = (setArtifactsStore) => {
const [match, setMatch] = useState("Hello");
const subhook = () => {
console.log("do some stuff with state");
setMatch("subhook")
};
return {match, sunhook};
};
const useRefreshArtifacts = (setArtifactsStore) => {
const [match, setMatch] = useState("Hello");
const refresh = () => {
console.log("do some stuff with state");
setMatch("refresh")
let h = someSubHook("sub");
h.subook()
};
return {match, refresh};
};
function ArtifactApp(props) {
const {match, refresh} = useRefreshArtifacts("");
return (<div><button onClick={refresh}>{match}</button></div>);
}
const AppContainer = () => {
return (<div><ArtifactApp /></div>);
}
ReactDOM.render(<AppContainer />, document.getElementById("root"));
https://jsfiddle.net/wu5ej3Lm/
Calling this throws an Invalid hook call. Hooks can only be called inside of the body of a function component.
Basically the codebase I have has a hook that performs some logic, and then has another hook that does some http post requests, both of which use state. I've inherited this so I'm finding my way through it, but I'm not sure if sticking all this as hooks is really best practice if there is a dependency on the state management.
Try to modify code in this way:
const {useEffect, useState, useRef} = React;
const someSubHook = (setArtifactsStore) => {
const [match, setMatch] = useState("Hello");
const subhook = () => {
console.log("do some stuff with state from subhook");
setMatch("subhook")
};
return {match, subhook};
};
const useRefreshArtifacts = (setArtifactsStore) => {
const [match, setMatch] = useState("Hello");
const {match_subhook, subhook} = someSubHook("sub");
const refresh = () => {
console.log("do some stuff with state from refresh");
setMatch("refresh")
subhook()
};
return {match, refresh};
};
function ArtifactApp(props) {
const {match, refresh} = useRefreshArtifacts("");
return (<div><button onClick={refresh}>{match}</button></div>);
}
const AppContainer = () => {
return (<div><ArtifactApp /></div>);
}
ReactDOM.render(<AppContainer />, document.getElementById("root"));
I have:
move let h = someSubHook("sub"); outside refresh function in this way const {match_subhook, subhook} = someSubHook("sub");
called subhook() in refresh function.
Here your fiddle modified.

Problems with debounce in useEffect

I have a form with username input and I am trying to verify if the username is in use or not in a debounce function. The issue I'm having is that my debounce doesn't seem to be working as when I type "user" my console looks like
u
us
use
user
Here is my debounce function
export function debounce(func, wait, immediate) {
var timeout;
return () => {
var context = this, args = arguments;
var later = () => {
timeout = null;
if (!immediate) func.apply(context, args);
};
var callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) func.apply(context, args);
};
};
And here is how I'm calling it in my React component
import React, { useEffect } from 'react'
// verify username
useEffect(() => {
if(state.username !== "") {
verify();
}
}, [state.username])
const verify = debounce(() => {
console.log(state.username)
}, 1000);
The debounce function seems to be correct? Is there a problem with how I am calling it in react?
Every time your component re-renders, a new debounced verify function is created, which means that inside useEffect you are actually calling different functions which defeats the purpose of debouncing.
It's like you were doing something like this:
const debounced1 = debounce(() => { console.log(state.username) }, 1000);
debounced1();
const debounced2 = debounce(() => { console.log(state.username) }, 1000);
debounced2();
const debounced3 = debounce(() => { console.log(state.username) }, 1000);
debounced3();
as opposed to what you really want:
const debounced = debounce(() => { console.log(state.username) }, 1000);
debounced();
debounced();
debounced();
One way to solve this is to use useCallback which will always return the same callback (when you pass in an empty array as a second argument). Also, I would pass the username to this function instead of accessing the state inside (otherwise you will be accessing a stale state):
import { useCallback } from "react";
const App => () {
const [username, setUsername] = useState("");
useEffect(() => {
if (username !== "") {
verify(username);
}
}, [username]);
const verify = useCallback(
debounce(name => {
console.log(name);
}, 200),
[]
);
return <input onChange={e => setUsername(e.target.value)} />;
}
Also you need to slightly update your debounce function since it's not passing arguments correctly to the debounced function.
function debounce(func, wait, immediate) {
var timeout;
return (...args) => { <--- needs to use this `args` instead of the ones belonging to the enclosing scope
var context = this;
...
demo
Note: You will see an ESLint warning about how useCallback expects an inline function, you can get around this by using useMemo knowing that useCallback(fn, deps) is equivalent to useMemo(() => fn, deps):
const verify = useMemo(
() => debounce(name => {
console.log(name);
}, 200),
[]
);
I suggest a few changes.
1) Every time you make a state change, you trigger a render. Every render has its own props and effects. So your useEffect is generating a new debounce function every time you update username. This is a good case for useCallback hooks to keep the function instance the same between renders, or possibly useRef maybe - I stick with useCallback myself.
2) I would separate out individual handlers instead of using useEffect to trigger your debounce - you end up with having a long list of dependencies as your component grows and it's not the best place for this.
3) Your debounce function doesn't deal with params. (I replaced with lodash.debouce, but you can debug your implementation)
4) I think you still want to update the state on keypress, but only run your denounced function every x secs
Example:
import React, { useState, useCallback } from "react";
import "./styles.css";
import debounce from "lodash.debounce";
export default function App() {
const [username, setUsername] = useState('');
const verify = useCallback(
debounce(username => {
console.log(`processing ${username}`);
}, 1000),
[]
);
const handleUsernameChange = event => {
setUsername(event.target.value);
verify(event.target.value);
};
return (
<div className="App">
<h1>Debounce</h1>
<input type="text" value={username} onChange={handleUsernameChange} />
</div>
);
}
DEMO
I highly recommend reading this great post on useEffect and hooks.
export function useLazyEffect(effect: EffectCallback, deps: DependencyList = [], wait = 300) {
const cleanUp = useRef<void | (() => void)>();
const effectRef = useRef<EffectCallback>();
const updatedEffect = useCallback(effect, deps);
effectRef.current = updatedEffect;
const lazyEffect = useCallback(
_.debounce(() => {
cleanUp.current = effectRef.current?.();
}, wait),
[],
);
useEffect(lazyEffect, deps);
useEffect(() => {
return () => {
cleanUp.current instanceof Function ? cleanUp.current() : undefined;
};
}, []);
}
A simple debounce functionality with useEffect,useState hooks
import {useState, useEffect} from 'react';
export default function DebounceInput(props) {
const [timeoutId, setTimeoutId] = useState();
useEffect(() => {
return () => {
clearTimeout(timeoutId);
};
}, [timeoutId]);
function inputHandler(...args) {
setTimeoutId(
setTimeout(() => {
getInputText(...args);
}, 250)
);
}
function getInputText(e) {
console.log(e.target.value || "Hello World!!!");
}
return (
<>
<input type="text" onKeyDown={inputHandler} />
</>
);
}
I hope this works well. And below I attached vanilla js code for debounce
function debounce(cb, delay) {
let timerId;
return (...args) => {
if (timerId) clearTimeout(timerId);
timerId = setTimeout(() => {
cb(...args);
}, delay);
};
}
function getInputText(e){
console.log(e.target.value);
}
const input = document.querySelector('input');
input.addEventListener('keydown',debounce(getInputText,500));
Here's a custom hook in plain JavaScript that will achieve a debounced useEffect:
export const useDebounce = (func, timeout=100) => {
let timer;
let deferred = () => {
clearTimeout(timer);
timer = setTimeout(func, timeout);
};
const ref = useRef(deferred);
return ref.current;
};
export const useDebouncedEffect = (func, deps=[], timeout=100) => {
useEffect(useDebounce(func, timeout), deps);
}
For your example, you could use it like this:
useDebouncedEffect(() => {
if(state.username !== "") {
console.log(state.username);
}
}, [state.username])

Resources