cribbble

React Hooks: useImmutableCallback

avatar
Ward Poel

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.