Memoize function argument in custom hook - reactjs

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>;
}

Related

Need to refer to latest state inside event handler registered from useEffect on load

useEvent solves the problem of reading latest props/state in a callback inside useEffect, but can't be used in production yet [Nov 22].
It's use case is also documented in beta docs as well
The problem
const SomeComponent = ({ prop1, ...}) => {
const [state, setState] = useState('initial')
useEffect(() => {
// inside the connect callback I want to refer to latest state and props and I dont want to reconnect on state change
// here event handler, depends on prop1 and state
const connection = createConnection(...)
connection.on('connect', () => {
// will refer to prop, state
// without these varaibles in depedency array
// this effect will not see the latest values
})
return () => {
connection.disconnect()
}
}, [])
useEffect depends on depends on prop1 and state, causing unnecessary reconnections.
Some patch work like solution using useRef
const SomeComponent = ({ prop1, ...}) => {
const [state, setState] = useState()
const someFunction = () => {
// use latest props1
// use latest state
}
const someFunctionRef = useRef()
someFunctionRef.current = someFunction;
useEffect(() => {
const someFunctionRefWrapper = () => {
someFunctionRef.current()
}
// need to read the latest props/state variables
// but not rerender when those values change
const connection = createConnection(...)
connection.on('connect', someFunctionRefWrapper)
return () => {
connection.disconnect()
}
}, [])
Right now useEvent can't be used in production, I am thinking of creating a custom hook to solve the problem
const usePoorMansUseEvent(callback) {
const itemRef = useRef();
itemRef.current = callback;
const stableReference = useCallback((..args) => {
itemRef.current(...args)
}, []);
return stableReference;
}
Is there any better approach, am I reinventing the wheel
You should be able to just make two useEffects, one for connecting / disconnecting and one for refreshing the callback.
If you just want a connection on mount:
const [connection] = useState(createConnection(...));
useEffect(() => {
return () => {
connection.disconnect()
}
}, [connection])
useEffect(() => {
connection.on('connect', someCallback)
return () => {
// Disconnect previous callback on change, idk the actual syntax
connection.off('connect', someCallback)
}
}, [connection, someCallback]
That follows the whole immutability principle.

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

How to disable React Hook "useCallback" cannot be called inside a callback warning?

I have created a utility hook that allow updating the state of components as long as they are mounted.
to do so, I had to call useCallack inside the callback function of Array.map
this is my code
export const useSafeDispatches = (...dispatches) => {
const mounted = useRef(false);
useLayoutEffect(() => {
mounted.current = true;
return () => (mounted.current = false);
}, []);
return dispatches.map((dispatch) =>
useCallback((...args) => (mounted.current ? dispatch(...args) : void 0), [dispatch])
);
};
I am getting this error when I try to build
React Hook "useCallback" cannot be called inside a callback. React Hooks must be called in a React function component or a custom React Hook function react-hooks/rules-of-hooks
The error message says exactly what you need to do, pull useCallback out of the map callback. Your callback depends on the value of mounted.current, so we make sure to include it in our list of dependencies -
export const useSafeDispatches = (...dispatches) => {
const mounted = useRef(false);
useLayoutEffect(() => {
mounted.current = true;
return () => (mounted.current = false);
}, []);
const safeDispatch = useCallback(dispatch =>
(...args) => mounted.current ? dispatch(...args) : void 0
, [mounted.current]);
return dispatches.map(safeDispatch);
};

How to trigger callback using useState synchronously if callback was external

I have the following hook:
const MessageStorage = () => {
const [messages, setMessages] = useState([]);
const addMessage = (message) => { ... }
const reset = (callback) => {
setMessages([])
callback(); // wrong since setMessages([]) is asynchronous
}
return { addMessage, reset };
}
The problem is with reset function. The standard approach to triggering callback is to use useEffect to track the change of state. But the problem is that callback is defined by the consumer of MessageStorage hook, and it must be defined as a parameter of reset function.
What's the best way to tackle this?
One approach is to put the callback into state:
const [callback, setCallback] = useState();
// ...
const reset = (callback) => {
setMessages([])
setCallback(callback);
}
useEffect(() => {
callback?.();
setCallback(); // make sure callback only gets called once
}, [messages]);

React useEffect Invalid hook call. Hooks can only be called inside of the body of a function component

I always get this problem when using useEffect but I see others do it just fine. Here is my Code.
Component 1 (Parent):
const UsePrevious = (value)=> {
const ref = useRef(0);
useEffect(() => {
console.log(value)
ref.current = value;
});
return ref.current;
}
const FetchAndHighlight = (index) => {
const prevAmount = UsePrevious(index);
useEffect(() => {
if(prevAmount !== index) {
getNote(props.data.company_id)
.then(data=>{
return setNotes(data)
})
.catch(err=>console.error(err))
}
}, [index])
Component 2 (child)
function handleDelete (row){
console.log(filteredItems)
const index = filteredItems.findIndex(r=>r._id===row);
props.FetchAndHighlight(index);
deleteNote(row)
console.log(row)
toast.info('Note deleted sucessfully.')
if(props.combined === 0){props.setCombined(1)} else{props.setCombined(0)}
}
You can't pass hooks through props and more importantly, you must use hooks on top level and not in functions like handleDelete instead import the hook and use it:
// export
export const useFetchAndHighlight = (index) => {...}
// usage
import { useFetchAndHighlight } from './useFetchAndHighlight.js';
// Don't use inside function
useFetchAndHighlight(index);

Resources