React Hooks: useImmutableCallback
Usage
The usage of this hook is very easy, you just pass a callback as parameter. The function the hook is returning will never change, but will always be executed with the newest variables.
Example
Let's say we have a chat application that works with web sockets. We create our connection inside an useEffect
. Each time we receive a message, we add the received message to an array of received messages. So we create a function onMessageReceive
with an event as parameter. If we would just create a normal function, the useEffect
would rerun after each render.
// Example with normal function
export default function BadChatExample() {
let [messages, setMessages] = useState([]);
function onMessageReceive(event) {
let message = JSON.parse(event.data);
setMessages([...messages, message]);
}
useEffect(() => {
let connection = new WebSocket(url);
connection.addEventListener('message', onMessageReceive);
return () => {
connection.close();
connection.removeEventListener('message', onMessageReceive);
}
}, [onMessageReceive])
}
If we create our component like this, the useEffect
cleanup function will run and we create a new connection and add a new event listener. This is definitely not what we want.
If we use React's useCallback
hook, we need to pass the messages
as dependency so React will recreate the onMessageReceive
each time we receive a new message because we add a new message to the array. This will result the same as a normal function, the useEffect
will rerun every time the onMessageReceive
function is recreated. Also not the solution we are looking for.
// Bad example with React useCallback
export default function BadChatExample() {
let [messages, setMessages] = useState([]);
let onMessageReceive = useCallback((event) {
let message = JSON.parse(event.data);
setMessages([...messages, message]);
}, [messages])
useEffect(() => {
let connection = new WebSocket(url);
connection.addEventListener('message', onMessageReceive);
return () => {
connection.close();
connection.removeEventListener('message', onMessageReceive);
}
}, [onMessageReceive])
}
With our custom useImmutableCallback
hook, the onMessageReceive
function will always be the same object, so React will not rerun the useEffect
each time. 🥳
// Good example with useImmutableCallback
export default function Chat() {
let [messages, setMessages] = useState([]);
let onMessageReceive = useImmutableCallback((event) => {
let message = JSON.parse(event.data);
setMessages([...messages, message]);
})
useEffect(() => {
let connection = new WebSocket(url);
connection.addEventListener('message', onMessageReceive);
return () => {
connection.close();
}
}, [onMessageReceive])
}
The hook
import { useRef, useCallback } from 'react';
export default function useImmutableCallback(callback) {
let callbackRef = useRef();
callbackRef.current = callback;
return useCallback(
function (...args) {
return callbackRef.current(...args);
},
[callbackRef],
);
}
If you have any questions, I'm @WardPoel on Twitter.