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
Related
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.
I'm building a custom hook that accepts a function. If the function will not change between re-renders/updates, where should I memoize the function?
OPTION 1
const useCustomHook = (callback) => {
const callbackRef = useRef();
callbackRef.current = callback;
// No effect, this will be re-created every time this hook is called
// because a new instance of callback is being passed
const callbackWrapper = useCallback(() => {
if (callbackRef.current) {
callbackRef.current();
}
}, [callbackRef]);
// use callbackWrapper
}
const Component = () => {
// New instance of passed callback will be created each time this component re-renders
useCustomHook(() => {
console.log(`I'm being passed to the hook`);
});
// ...
// ...
return <div></div>;
}
OPTION 2
const useCustomHook = (callback) => {
// Callback is already memoized
const callbackRef = useRef();
callbackRef.current = callback;
// use callbackRef
}
const Component = () => {
// Memoized function passed, but
// 1. Is this allowed?
// 2. Requires more effort by users of the hook
useCustomHook(useCallback(() => {
console.log(`I'm being passed to the hook`);
}, []));
// ...
// ...
return <div></div>;
}
Option 2 seems more valid but it requires users of the custom hook to first enclose their function in a useCallback() hook. Is there an alternative way where users don't need to enclose the passed function in useCallback()?
I would probably use the useEffect hook to "memoize" the callback to the React ref.
const useCustomHook = (callback) => {
const callbackRef = useRef();
useEffect(() => {
// only update callback saved in ref if callback updates
callbackRef.current = callback;
}, [callback]);
// provide stable callback that always calls latest
// callback stored in ref
const callbackWrapper = useCallback(() => {
if (callbackRef.current) {
callbackRef.current();
}
}, []);
// use callbackWrapper
}
Or use useMemo or useCallback directly, but at this point you are essentially just redefining useMemo or useCallback in your custom hook. In this case, pass an additional dependency array argument.
const useCustomHook = (callback, deps) => {
const callbackWrapper = useMemo(() => callback, deps);
// use callbackWrapper
}
This being said, you will likely want to be in the habit of memoizing callbacks that are being passed down before they are passed, in order to guaranteed provided stable references. This way you won't need to go through the rigamarole of the weirdness of your custom hook.
Another other way is to declare the callback outside of the component body ( but I don't recommend that, It can create some edge cases )
function cb () {
console.log(`I'm being passed to the hook`);
}
const Component = () => {
useCustomHook(cb);
// ...
// ...
return <div></div>;
}
memoize the function in you want on top level component body and then pass it to your custom hook like this:
const Component = () => {
// hooks should be called on top level function body
const cb = useCallback(() => {
console.log(`I'm being passed to the hook`);
}, []);
useCustomHook(cb);
// ...
// ...
return <div></div>;
}
I've got stuck with a problem of hook reusage when state and redux store should work together. I've simplified a code to show the problem.
There is a component where I want to use multiple hooks (for simplicity I re-use useMouseDown hook here):
export function Counter() {
const count = useSelector(selectCount);
const dispatch = useDispatch();
const plusSighRef = useRef<HTMLButtonElement>(null);
useMouseDown({
ref: plusSighRef,
onMouseDown: () => {
console.log('in first hook');
dispatch(increment());
}
});
useMouseDown({
ref: plusSighRef,
onMouseDown: () => { console.log('in second hook'); }
});
return <button ref={plusSighRef}>+</button>;
}
Each hook has inner state and has own callback on mouse down event:
const useMouseDown = ({ ref, onMouseDown }) => {
const [isClicked, setIsClicked] = useState(false);
useEffect(() => {
const element = ref.current;
const down = (e: MouseEvent) => {
setIsClicked(true);
onMouseDown(e);
}
element.addEventListener('mousedown', down);
return (): void => {
element.removeEventListener('mousedown', down);
};
}, [onMouseDown, ref]);
}
As a result a mousedown event in second hook is never triggered. The problem is that re-render occurs earlier than second hook is started.
I found some solutions but don't like both:
use something like setTimeout(() => dispatch(increment()), 0) inside the first hook mousedown prop. But it seems to be not obvious in terms of re-usage.
rewrite two hooks into one and manipulate with one "big" mousedown handler. But in that case a combined hook could be difficult for maintaining.
So I need a solution that allow to retain structure as is (I mean two separate hooks), but has second hook is working too. Could someone help how to get it?
Can't you just keep your logic in two different functions and execute it sequentially in the hook's onMouseDown function?
const executeFirstFlow = () => {
console.log('in first function');
dispatch(increment());
};
const executeSecondFlow = () => {
console.log('in second function');
};
useMouseDown({
ref: plusSighRef,
onMouseDown: () => {
executeFirstFlow();
executeSecondFlow();
}
});
i have a function that use in useEffect and click event, then throw warning "React Hook useEffect has a missing dependency", how should i remove the warning?
// location is react router location
const Component = ({ location }) => {
const [data, setData] = useState(null);
const fetchData = () => {
const { id } = parseLocation(location);
fetchDataFromServer(id).then(data => setData(data));
}
useEffect(() => {
fetchData();
}, [location]);
return (
<div>
{data}
<button onClick={fetchData)}>reload</button>
</div>
);
}
then i try this, but the warning still exist
// location is react router location
const Component = ({ location }) => {
const [data, setData] = useState(null);
const fetchData = (l) => {
// l is location
const { id } = parseLocation(l);
fetchDataFromServer(id).then(data => setData(data));
}
useEffect(() => {
fetchData(location);
}, [location]);
return (
<div>
{data}
<button onClick={() => fetchData(location)}>reload</button>
</div>
);
}
The point of the exhaustive-deps rule is to prevent hooks from reading stale props or state. The issue is that since fetchData is defined within the component, as far as the linter is concerned, it could be accessing stale props or state (via closure).
One solution is to pull fetchData out of the component and pass it everything it needs: (it's already being passed the location):
const fetchData = (l, setData) => {
// l is location
const { id } = parseLocation(l);
fetchDataFromServer(id).then(data => setData(data));
}
const Component = ({ location }) => {
const [data, setData] = useState(null);
useEffect(() => {
fetchData(location, setData);
}, [location]);
return (
<div>
{data}
<button onClick={() => fetchData(location, setData)}>reload</button>
</div>
);
}
Since fetchData isn't defined outside the component, the linter knows that it won't access state or props, so it isn't an issue for stale data.
To be clear, though, your original solution is correct, from a runtime perspective, since fetchData doesn't read state or props - but the linter doesn't know that.
You could simply disable the linter, but it'd be easy to accidentally introduce bugs later on (if fetchData is ever modified) that way. It's better to have the linter rule verifying correctness, even if it means some slight restructuring of code.
Alternative solution
An alternative solution which leverages closure instead of passing the location into fetchData:
const Component = ({ location }) => {
const [data, setData] = useState(null);
const fetchData = useCallback(() => {
// Uses location from closure
const { id } = parseLocation(location);
fetchDataFromServer(id).then(data => setData(data));
// Ensure that location isn't stale
}, [location]);
useEffect(() => {
fetchData();
// Ensure that fetchData isn't stale
}, [fetchData]);
return (
<div>
{data}
<button onClick={fetchData}>reload</button>
</div>
);
}
This approach lets you avoid passing location into fetchData every time you call it. However, with this approach it's important to make sure to avoid stale state and props.
If you omitted the [fetchData] dep to useEffect, the effect would only run once, and new data wouldn't be fetched when location changed.
But if you have fetchData in the deps for useEffect but don't wrap fetchData in useCallback, the fetchData function is a new function every render, which would cause the useEffect to run every render (which would be bad).
By wrapping in useCallback, the fetchData function is only a new function whenever the location changes, which causes the useEffect to run at the appropriate point.
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);
});
}, []);
}