Cleanup is not only an unmount hook — it runs before every re-run of the effect, which is what makes a subscription with changing dependencies correct.
useEffect(() => {
const socket = connect(roomId)
return () => socket.close()
}, [roomId])
// roomId changes:
// 1. cleanup runs, closing the OLD socket
// 2. the effect runs, opening the new one
//
// without the cleanup, changing rooms four times leaves
// four open sockets and four sets of handlers.
The ordering is the useful part: the old resource is released before the new one is acquired, so there is no window where both exist. The leak this prevents is invisible in a test that mounts once and is obvious in a component whose dependency changes — a chat room, a filtered subscription, a polling interval keyed on a selection. Any effect that acquires something and does not return a cleanup deserves a second look.