Lesson 16 of 30
Opening a WebSocket is the easy part — the real implementation work is making its connection lifecycle behave inside a component's mount/unmount lifecycle instead of quietly outliving it.
13 min read
Every option explains itself — why the right one is right, and why each wrong one is wrong. Miss one and it comes back later on a spaced-repetition schedule.
Question 1 of 3
A component opens a WebSocket inside a useEffect but the effect has no cleanup function. What happens when the component unmounts?
Hints without spoilers, and a plain explanation of why a wrong answer was wrong — answered only from this lesson. Free account, no card.
Sign up freeWhat interviewers actually ask about this, and what a strong answer has to cover.
A strong answer covers
Likely follow-ups
An earlier lesson in this app covers what a WebSocket mechanically is — a persistent, full-duplex connection that starts as HTTP and upgrades. This lesson is about the part that actually breaks in real apps: wiring that connection correctly into a React component's lifecycle.
Three concrete things go wrong in practice, in order of how often they show up:
WebSocket opened when a component mounts has to be closed when it unmounts. Forget the cleanup step and the socket keeps running after the component is gone — a real, common connection leak, not a theoretical one.function useChatFeed(roomId: string) {
const [messages, setMessages] = useState<ChatMessage[]>([]);
const [status, setStatus] = useState<"connecting" | "open" | "reconnecting">("connecting");
useEffect(() => {
let socket: WebSocket;
let reconnectTimer: ReturnType<typeof setTimeout>;
let reconnectDelay = 1000;
let unmounted = false;
function connect() {
socket = new WebSocket(`wss://api.example.com/rooms/${roomId}`);
socket.onopen = () => {
setStatus("open");
reconnectDelay = 1000; // reset backoff after a successful connect
};
socket.onmessage = (event) => {
const message: ChatMessage = JSON.parse(event.data);
// Functional update reads the LATEST messages array, not whatever
// array existed when this handler was first attached.
setMessages((prev) => [...prev, message]);
};
socket.onclose = () => {
if (unmounted) return;
setStatus("reconnecting");
reconnectTimer = setTimeout(connect, reconnectDelay);
reconnectDelay = Math.min(reconnectDelay * 2, 30000);
};
}
connect();
return () => {
unmounted = true;
clearTimeout(reconnectTimer);
socket.close();
};
}, [roomId]);
return { messages, status };
}
The cleanup function is doing two jobs at once: it closes whatever socket is currently open, and it flips an unmounted flag so a reconnect attempt that was already scheduled doesn't fire and open a brand-new connection after the component is gone. Skip either half and you get a leak — either a socket nobody's listening to anymore, or a reconnect loop that keeps recreating one.
useEffect's cleanup function is not optional cleanup — it's the only place a socket should close. Closing a socket from an unrelated event handler or a manual "disconnect" button click, instead of the effect's own return function, means the effect's own lifecycle and the socket's actual lifecycle drift apart."How would you handle a live data feed in a React component?" is a real, common systems question, and naming the cleanup-on-unmount step, backoff on reconnect, and the stale-closure risk unprompted is what separates someone who's actually shipped this from someone who's only called new WebSocket() once in a demo.