skip to content

In React, why is const socket = useMemo(() => new WebSocket(url), [url]) the wrong way to give a component one long-lived connection, and what would you do instead?

level: seniorimportance: should knowfreq 40%

answer

  1. what closes it when the cache is dropped?
  2. no cleanup channel on this hook
  3. construction during render is a side effect
  4. refs and state are preserved; memo is not
  5. setup and teardown belong in an effect

basics

~20 s

React may discard a useMemo cache and re-run the factory, so that line can open extra sockets, and opening one during render is a side effect with no teardown. Create the socket in an effect keyed on url, hold it in a ref, and close it in the cleanup.

solid answer

~60 s

Two things are wrong with it. First, `useMemo` is a performance hint: React may drop the cached value and call the factory again with `url` unchanged, which opens a second connection while the first stays open, because `useMemo` has no cleanup channel to close anything. Second, the factory runs during the render phase, and rendering must be pure — React can start a render and throw it away before committing, so you may open a connection for output that never reaches the screen. The correct shape is `useEffect` with `[url]`: construct the socket inside the effect, keep it in a `useRef` if event handlers need it, and return a cleanup that calls `socket.close()`. That gives you exactly one connection per committed `url`, closed when `url` changes and on unmount. For a purely in-memory object with no teardown — a `Map` used as a scratch cache — a lazily initialized ref or a `useState` lazy initializer is the honest way to keep one instance, since React preserves state and refs where it does not promise to preserve a memo.

code

javascript · 17 lines
javascript
import { useEffect, useRef } from 'react';

function Feed({ url }) {
  const socketRef = useRef(null);

  useEffect(() => {
    const socket = new WebSocket(url);
    socketRef.current = socket;
    socket.addEventListener('message', (e) => console.log(e.data));
    return () => {
      socketRef.current = null;
      socket.close();
    };
  }, [url]);

  return null;
}

go deeper

for a junior

Recall that anything needing to be closed or cancelled is set up in an effect and torn down in the function that effect returns. useMemo has no place to put that teardown.

for a middle

Explain both defects: the cache may be discarded so the factory can run again, and the factory runs during render, where side effects are not allowed. Then write the effect-plus-ref version correctly.

for a senior

Show the diagnostic habit — 'does this need to be destroyed?' decides the hook before any caching argument does — and distinguish storage React guarantees to preserve (state, refs) from storage it merely tries to preserve.

for a principal

Own the rule at codebase level: resource ownership must have exactly one lifecycle owner with a teardown, and reviewers should treat resource construction inside a render-phase hook as a defect class rather than a style debate.

## The two independent defects **The cache is not a lifetime guarantee.** React documents `useMemo` as a performance optimization and reserves the right to discard cached values. If it does, the factory runs again and constructs a second `WebSocket`. Nothing closes the first one — `useMemo` has no cleanup return value, and React has no idea the value it dropped owned an external resource. The failure mode is the worst kind: intermittent, environment-dependent, and invisible in a quick test. **Construction is a side effect in the render phase.** The factory executes while the component renders, and rendering must be pure. React may render a component and discard the result — during concurrent rendering an in-progress render can be abandoned when something higher priority arrives, and in development React deliberately re-runs component bodies to surface impurity. Opening a socket there means opening it at a moment that does not correspond to anything being on screen. There is also a third, quieter problem: `url` changing gives you a new socket, but no one ever closed the old one, so the component leaks a connection per navigation. ## The shape that is correct Effects exist precisely for "set something up, tear it down again": ```js import { useEffect, useRef } from 'react'; function Feed({ url }) { const socketRef = useRef(null); useEffect(() => { const socket = new WebSocket(url); socketRef.current = socket; return () => { socketRef.current = null; socket.close(); }; }, [url]); return null; } ``` Every property the original was reaching for is now actually guaranteed. The socket is created after commit, so it belongs to a component that really is on screen. When `url` changes, React runs the cleanup for the old value before the setup for the new one, so the old connection closes first. On unmount, the cleanup runs. The ref gives synchronous access to the current instance from handlers without putting the socket in state. Note what the ref is *not* doing: it is not creating the socket. `useRef(new WebSocket(url))` is a distinct bug — the argument expression is evaluated on every render, so it constructs a socket each time even though only the first one is ever stored. ## When the object has no teardown Not everything is a connection. Suppose you want one `Map` per component instance to use as a scratch cache. There is nothing to close, so the only question is whether losing it matters: - If losing it is harmless (it is a cache, and recomputing is fine), `useMemo` is acceptable in spirit — but so is simply not memoizing. - If the instance must survive for the component's lifetime because state accumulates in it, use storage React actually preserves. A lazily initialized ref does it without allocating on every render: ```js const cacheRef = useRef(null); if (cacheRef.current === null) { cacheRef.current = new Map(); } ``` A `useState` lazy initializer — `useState(() => new Map())[0]` — is the other standard option; React calls the initializer only on mount and keeps the value as state. The distinction to hold on to: React promises to preserve **state** and **refs** across renders of a mounted component. It only *tries* to preserve memoized values. ## The general test Before any `useMemo`, ask: *if this factory ran on every render, would the component still be correct?* For pure derivations — filtering, sorting a copy, deriving a display shape — the answer is yes, and the hook is being used as designed. For anything that constructs an owner of an external resource, registers something, or accumulates state, the answer is no, and the value belongs in an effect plus a ref, or in state. A second, sharper test: *does this thing need to be destroyed?* If yes, it needs a cleanup, and the only hooks with cleanups are `useEffect` and `useLayoutEffect`. That alone rules `useMemo` out before the caching question is even reached. ## What an interviewer is listening for Candidates who have only read the API often answer "use `useRef`" and stop. The complete answer names both defects — the discardable cache *and* the impure render — and it identifies teardown as the deciding factor, because teardown is what makes the choice unambiguous rather than a matter of taste.

  • What if the value has no teardown at all, like a Map used as a scratch cache — is useMemo fine then?
    It is defensible only if losing it is harmless. If state accumulates in that Map and must survive, use storage React actually preserves: a lazily initialized ref (`if (ref.current === null) ref.current = new Map();`) or a `useState` lazy initializer. React promises to keep refs and state; it only tries to keep memoized values.
  • Why is useRef(new WebSocket(url)) also wrong?
    The argument expression is evaluated on every render, so a socket is constructed each time even though React keeps only the first one. Every later instance is created and immediately orphaned. Initialize lazily inside a guard, or better, create the socket in an effect where a cleanup can close it.
  • With the socket owned by an effect, how do you reconnect when url changes?
    List `url` in the effect's dependency array. React runs the previous cleanup — closing the old socket — before running the setup with the new URL, so you get exactly one live connection per committed URL, in the right order, with no extra bookkeeping.
  • How would you expose the connection to an event handler in the same component?
    Store it in a ref inside the effect and read `ref.current` from the handler, clearing the ref in the cleanup so a handler that fires after teardown sees `null` rather than a closed socket. Keep it out of state — the instance never needs to trigger a re-render by itself.

saying these in an interview costs you the question

  • Says useMemo with [url] guarantees exactly one socket for that URL
  • Opens connections during render and closes them nowhere
  • Writes useRef(new WebSocket(url)), constructing one on every render
  • Assumes React tears down a memoized value it discarded
  • Believes an unmounting component's sockets are closed by React automatically

context