react hooks: patterns when extract hook with callback function parameter - reactjs

I am extracting a custom hook with a onRes parameter;
function useApi(onRes) {
useEffect(() => {
api().then((res) => {
onRes && onRes(res);
});
}, [onRes]);
}
to use this hook:
import useApi from './useApi';
function App() {
const [x, setX] = useState(0);
useApi({
onRes: () => {}
})
return (
<div onClick={() => setX(Math.random())}>{x}</div>
)
}
notice that every time <App/> renders, onRes will change, and the useApi hooks will run again
my question is should wrap onRes with useCallback ? or I just inform the hook users to be careful with this onRes parameter ?
function useApi(onRes) {
const onResCb = useCallback(onRes, []); // should I do this ?
useEffect(() => {
api().then((res) => {
onResCb && onResCb(res);
});
}, [onResCb]);
}

Just remove onRes from the dependencies array of useEffect, this will make sure the effect will run only on mount
function useApi(onRes) {
useEffect(() => {
api().then((res) => {
onRes && onRes(res);
});
}, []);
}
second option, define the function that you pass in with useCallback with an empty dependencies array and pass it to useApi and keep your current hook the same
const onRes = useCallback(() => {
console.log('hi')
}, []);
useApi(onRes);

This blog post might be useful: When to useMemo and useCallback
I would think that using callback is not relevant here since you're not doing heavy computation. useCallback would actually be less performant than using the function as-is.
Besides, you can save an API call if onRes is undefined.
function useApi(onRes) {
useEffect(() => {
if(! onRes){
return
}
api().then(onRes);
}, [onRes]);
}

after reading this article,Refs to the Rescue!, I finally get inspired.
we can store onRes with a ref, and call it when needed.
function useApi(onRes) {
const savedCallback = useRef();
useEffect(() => {
savedCallback.current = onRes;
});
useEffect(() => {
api().then((res) => {
savedCallback.current && savedCallback.current(res);
});
}, []);
}

Related

Update useEffect returned function in a rerender without calling it

In the following code the returned function inside useEffect, is called when the component unmounts.
function MyComponent() {
const [variable, setVariable] = useState(1)
useEffect(() => {
return () => console.log(variable);
}, []);
setVariable(2);
}
But I want a thing different from the above code: I want that the definition of the returned function become updated when the variable value is changed, without calling it (except when it is time to unmount). What should I do?
You should set variable as a dependency for useEffect:
function MyComponent() {
const [variable, setVariable] = useState(1)
useEffect(() => {
return () => console.log(variable);
}, [variable]);
setVariable(2);
}
Therefore console.log runs whenever variable changes.
You can try this way
function MyComponent() {
const [variable, setVariable] = useState(1)
const [unmountingFunction, setUnmountingFunction] = useState(() => console.log(variable))
//whenever you update value, this unmounting function will be updated too
useEffect(() => {
setUnmountingFunction(() => console.log("Your unmounting function is here"))
}, [variable]);
useEffect(() => {
return unmountingFunction;
}, []);
setVariable(2);
}

React use-effect [duplicate]

I'm checking if a component is unmounted, in order to avoid calling state update functions.
This is the first option, and it works
const ref = useRef(false)
useEffect(() => {
ref.current = true
return () => {
ref.current = false
}
}, [])
....
if (ref.current) {
setAnswers(answers)
setIsLoading(false)
}
....
Second option is using useState, which isMounted is always false, though I changed it to true in component did mount
const [isMounted, setIsMounted] = useState(false)
useEffect(() => {
setIsMounted(true)
return () => {
setIsMounted(false)
}
}, [])
....
if (isMounted) {
setAnswers(answers)
setIsLoading(false)
}
....
Why is the second option not working compared with the first option?
I wrote this custom hook that can check if the component is mounted or not at the current time, useful if you have a long running operation and the component may be unmounted before it finishes and updates the UI state.
import { useCallback, useEffect, useRef } from "react";
export function useIsMounted() {
const isMountedRef = useRef(true);
const isMounted = useCallback(() => isMountedRef.current, []);
useEffect(() => {
return () => void (isMountedRef.current = false);
}, []);
return isMounted;
}
Usage
function MyComponent() {
const [data, setData] = React.useState()
const isMounted = useIsMounted()
React.useEffect(() => {
fetch().then((data) => {
// at this point the component may already have been removed from the tree
// so we need to check first before updating the component state
if (isMounted()) {
setData(data)
}
})
}, [...])
return (...)
}
Live Demo
Please read this answer very carefully until the end.
It seems your component is rendering more than one time and thus the isMounted state will always become false because it doesn't run on every update. It just run once and on unmounted. So, you'll do pass the state in the second option array:
}, [isMounted])
Now, it watches the state and run the effect on every update. But why the first option works?
It's because you're using useRef and it's a synchronous unlike asynchronous useState. Read the docs about useRef again if you're unclear:
This works because useRef() creates a plain JavaScript object. The only difference between useRef() and creating a {current: ...} object yourself is that useRef will give you the same ref object on every render.
BTW, you do not need to clean up anything. Cleaning up the process is required for DOM changes, third-party api reflections, etc. But you don't need to habit on cleaning up the states. So, you can just use:
useEffect(() => {
setIsMounted(true)
}, []) // you may watch isMounted state
// if you're changing it's value from somewhere else
While you use the useRef hook, you are good to go with cleaning up process because it's related to dom changes.
This is a typescript version of #Nearhuscarl's answer.
import { useCallback, useEffect, useRef } from "react";
/**
* This hook provides a function that returns whether the component is still mounted.
* This is useful as a check before calling set state operations which will generates
* a warning when it is called when the component is unmounted.
* #returns a function
*/
export function useMounted(): () => boolean {
const mountedRef = useRef(false);
useEffect(function useMountedEffect() {
mountedRef.current = true;
return function useMountedEffectCleanup() {
mountedRef.current = false;
};
}, []);
return useCallback(function isMounted() {
return mountedRef.current;
}, [mountedRef]);
}
This is the jest test
import { render, waitFor } from '#testing-library/react';
import React, { useEffect } from 'react';
import { delay } from '../delay';
import { useMounted } from "./useMounted";
describe("useMounted", () => {
it("should work and not rerender", async () => {
const callback = jest.fn();
function MyComponent() {
const isMounted = useMounted();
useEffect(() => {
callback(isMounted())
}, [])
return (<div data-testid="test">Hello world</div>);
}
const { unmount } = render(<MyComponent />)
expect(callback.mock.calls).toEqual([[true]])
unmount();
expect(callback.mock.calls).toEqual([[true]])
})
it("should work and not rerender and unmount later", async () => {
jest.useFakeTimers('modern');
const callback = jest.fn();
function MyComponent() {
const isMounted = useMounted();
useEffect(() => {
(async () => {
await delay(10000);
callback(isMounted());
})();
}, [])
return (<div data-testid="test">Hello world</div>);
}
const { unmount } = render(<MyComponent />)
await waitFor(() => expect(callback).toBeCalledTimes(0));
jest.advanceTimersByTime(5000);
unmount();
jest.advanceTimersByTime(5000);
await waitFor(() => expect(callback).toBeCalledTimes(1));
expect(callback.mock.calls).toEqual([[false]])
})
})
Sources available in https://github.com/trajano/react-hooks-tests/tree/master/src/useMounted
This cleared up my error message, setting a return in my useEffect cancels out the subscriptions and async tasks.
import React from 'react'
const MyComponent = () => {
const [fooState, setFooState] = React.useState(null)
React.useEffect(()=> {
//Mounted
getFetch()
// Unmounted
return () => {
setFooState(false)
}
})
return (
<div>Stuff</div>
)
}
export {MyComponent as default}
If you want to use a small library for this, then react-tidy has a custom hook just for doing that called useIsMounted:
import React from 'react'
import {useIsMounted} from 'react-tidy'
function MyComponent() {
const [data, setData] = React.useState(null)
const isMounted = useIsMounted()
React.useEffect(() => {
fetchData().then((result) => {
if (isMounted) {
setData(result)
}
})
}, [])
// ...
}
Learn more about this hook
Disclaimer I am the writer of this library.
Near Huscarl solution is good, but there is problem with using these hook with react router, because if you go from example news/1 to news/2 useRef value is set to false because of unmount, but value keep false. So you need init ref value to true on each mount.
import {useRef, useCallback, useEffect} from "react";
export function useIsMounted(): () => boolean {
const isMountedRef = useRef(true);
const isMounted = useCallback(() => isMountedRef.current, []);
useEffect(() => {
isMountedRef.current = true;
return () => void (isMountedRef.current = false);
}, []);
return isMounted;
}
It's hard to know without the larger context, but I don't think you even need to know whether something has been mounted. useEffect(() => {...}, []) is executed automatically upon mounting, and you can put whatever needs to wait until mounting inside that effect.

useCallback wrap a simple function in ReactJS

I want to understand the utility of useCallback in ReactJs. I read that useCallback is used to memoise the function inside it, and to trigger the callback depending by dependecies. How i notice we should use this hook when pass a function as a prop. In the same time i found an example on the internet and i can't figure out why the hook is used.
const useAsync = () => {
const [data, setData] = useState(null)
const execute = useCallback(() => {
setLoading(true)
return asyncFunc()
.then(res => {
setData(res)
return res
})
}, [])
}
Why execute function is wrapped by this hook in this example? And in general should we use useCallback if we don't pass a function as a parameter in a compoenent?
Definition:
const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b],
);
Pass an inline callback and an array of dependencies. useCallback will return a memoized version of the callback that only changes if one of the dependencies has changed. This is useful when passing callbacks to optimized child components that rely on reference equality to prevent unnecessary renders (e.g. shouldComponentUpdate).
So yes it returns a memoized callback, but is basically used, in general, to factorize some redundant operations (like a call to an API).
In your case, suppose you have a useCallback like this:
const useAsync = (asyncFunc) => {
const [data, setData] = useState(null)
const execute = useCallback(() => {
return asyncFunc()
.then(res => {
setData(res)
return res
})
}, [asyncFunc])
return { execute, data };
}
Now let's use it in a component:
import React, { useEffect } from 'react';
function App() {
const { execute, data } = useAsync(myFunction);
useEffect(() => {
execute();
}, [execute]);
return (
<div>
{data.map(el => ...)}
</div>
);
}
Where myFunction is:
function myFunction() {
return fetch('http://localhost:3001/users/')
.then((response) => {
return response.json().then((data) => {
return data;
}).catch((err) => {
console.log(err);
})
});
}
Well, the result is that, data now are filled with the response coming from 'http://localhost:3001/users/' route.
Ok so now you could say "Yes but what's the difference between this verbose code and just a direct call to myFunction somewhere in the code?" and the answer is "this is a better approach because the callback is memoized (= will be taken in care by React that caches some operation to increase performances) and will change only if myFunction changes (I mean if you use another function because you have to fetch from another route)".
useCallback is used to prevent useless re-rendering of components or its child. If you know about React.memo(), useCallback is its functional equivalent.
Consider this:
const Foo = () => {
const handleClick = () => {
console.log('Clicked');
}
return <button onClick={handleClick}>Click Me</button>;
}
This will re-render the Foo component again and again even when it's not necessary.
Now consider this:
const Foo = () => {
const memoizedHandleClick =
useCallback(
() => console.log('Click happened')
,[]); // Tells React to memoize regardless of arguments.
return <Button onClick={memoizedHandleClick}>Click Me</Button>;
}
In this code, React will memoize the callback function and the callback will not be created multiple times hence no more useless re-renders

Should I memoize the returned object of a custom React hook?

If I used useCallback for the methods of my custom hook, should I also memoize the returned object? I would assume that it would not create a new object everytime since it's composed with memoized methods and primitives.
export const useToggle = (args: UseToggleProps) => {
const initialState=
args.initialState !== undefined ? args.initialState : false
const [active, setActive] = useState(initialState)
const toggleOff = useCallback(() => {
setActive(false)
}, [])
const toggleOn = useCallback(() => {
setActive(true)
}, [])
const toggle = useCallback(() => {
setActive((active) => !active)
}, [])
// Should this also be memoized with useMemo?
return {
active,
toggle,
toggleOff,
toggleOn
}
}
You don't need to memoize the returned object unless you are passing the Object directly to function of useEffect in which case reference will fail
You don't need to add an extra layer of memoization over useCallback if you use it like this:
const Comp = () => {
const { toggleOn, toggleOff } = useToggle();
useEffect(() => {
console.log('Something here')
}, [toggleOn]);
return (
<Child toggleOn={toggleOn} toggleOff={toggleOff} />
)
}
However the usages like below code will need memoization of the returned object
const Comp = () => {
const toggle = useToggle();
useEffect(() => {
console.log('Something here')
}, [toggle]);
return (
<Child toggleHandlers={toggle} />
)
}

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