skip to content

In a React Native screen, how do you open a WebSocket, handle its onopen, onmessage and onclose events, and close it cleanly?

level: juniorimportance: must knowfreq 48%

answer

  1. a global, no import needed
  2. send only after onopen
  3. close in the effect cleanup
  4. close() defaults to code 1000
  5. a failure: error, then close 1006

basics

~20 s

Create the global WebSocket in a useEffect, send only after onopen, parse event.data in onmessage and call ws.close() in the cleanup. React Native's send() throws while still connecting, and a failed connection fires error, then close with code 1006.

solid answer

~40 s

`WebSocket` is a global in React Native, backed by a native module, so I create it inside a `useEffect` and keep it in a ref. I attach `onopen` (now `send` is legal; React Native's `send` throws `INVALID_STATE_ERR` while the socket is still connecting), `onmessage` (parse `event.data`, a string for text frames), `onerror` and `onclose`, which receives a `code` and `reason`. The effect cleanup calls `ws.close()`, which sends code `1000` by default, and detaches handlers so a late message cannot update an unmounted screen. When the connection fails, React Native fires `error` and then `close` with code `1006`, and the native failure message arrives as the close event's `reason`. Because stack navigators keep earlier screens mounted, I decide deliberately whether a socket lives with a screen or in an app-level service.

code

tsx · 34 lines
tsx
import { useEffect, useRef, useState } from 'react';
import { Text } from 'react-native';

type Bid = { lotId: string; amount: number };

export function LotBids({ lotId }: { lotId: string }) {
  const [latest, setLatest] = useState<Bid | null>(null);
  const wsRef = useRef<WebSocket | null>(null);

  useEffect(() => {
    const ws = new WebSocket('wss://auctions.example.com/bids');
    wsRef.current = ws;
    ws.onopen = () => ws.send(JSON.stringify({ subscribe: lotId }));
    ws.onmessage = (e) => {
      try {
        setLatest(JSON.parse(e.data) as Bid);
      } catch {
        // ignore malformed frames
      }
    };
    ws.onclose = (e) => {
      if (e.code !== 1000) {
        console.warn('bids socket closed', e.code, e.reason);
      }
    };
    return () => {
      ws.onmessage = null;
      ws.onclose = null;
      ws.close();
    };
  }, [lotId]);

  return <Text>{latest ? `Top bid: ${latest.amount}` : 'Waiting for bids'}</Text>;
}

go deeper

for a junior

Recall the four events, that send must wait for onopen, and that the socket is created in an effect and closed in its cleanup.

for a middle

Explain React Native's failure sequence, error then close with 1006 and the reason on the close event, and why send throws while connecting.

for a senior

Decide between screen-scoped and app-scoped connections given stack navigators keeping screens mounted, and treat onclose as the single place for failure handling.

for a principal

Set a real-time architecture for the app: one multiplexed connection versus several, topic subscriptions, and how screens consume a shared stream without leaking connections.

## The WebSocket global in React Native React Native installs a `WebSocket` class globally, so no import is needed. It mirrors the browser API (`readyState`, `send`, `close`, `onopen`, `onmessage`, `onerror`, `onclose`) but is implemented by a **native module**: OkHttp on Android and SocketRocket on iOS. Events arrive from native code and are dispatched on the JavaScript thread. The lifecycle, in the order it happens: 1. `new WebSocket(url)` starts connecting; `readyState` is `CONNECTING` (0). 2. `open` fires when the handshake succeeds; `readyState` becomes `OPEN` (1). 3. `message` fires for each incoming message; `event.data` is a string for text and binary data for binary frames. 4. `close()` moves the socket to `CLOSING` and, once native reports it, `close` fires and `readyState` is `CLOSED` (3). ## Wiring it into a screen In the live-auction example, a lot screen subscribes to bids for one lot: - **Create in an effect, store in a ref.** Creating the socket during render would open a new connection on every render. - **Send only from `onopen` or later.** React Native's `send` throws an `Error('INVALID_STATE_ERR')` while the socket is still `CONNECTING`, so a subscribe message belongs in `onopen`. - **Parse defensively in `onmessage`.** A malformed message should be logged and skipped, not crash the screen. - **Close in cleanup.** The effect's cleanup calls `ws.close()`. With no arguments React Native sends status `1000` (normal closure) and an empty reason. Setting the handlers to `null` first stops a message that is already in flight from updating state after unmount. ## What failure looks like React Native's implementation reports a failed or dropped connection in a specific way that surprises people who only read the web docs: | Situation | Events you receive | Useful data | |---|---|---| | Server closes normally | `close` | `code` and `reason` from the server | | You call `close()` | `close` | `1000` unless you passed a code | | Handshake fails, network drops, TLS error | `error`, then `close` | `close.code` is `1006`; `close.reason` carries the native error message | The `error` event itself carries no details in React Native 0.87; read the **`reason` on the following `close` event** instead. The close event also does not expose `wasClean`. Because `close` fires in every case, it is the one place to decide whether to reconnect. ## Screen-scoped or app-scoped? React Navigation's stack keeps previous screens **mounted** when you push a new one. A socket owned by the auction list screen therefore stays open while the user views a lot detail, and pushing two lot screens can open two sockets. Choose on purpose: - **Screen-scoped** – simple; right when the stream matters only while that screen is visible. - **App-scoped service** – one connection in a module or context, with screens subscribing to topics over it; right for chat, notifications or auctions shown on several screens. During development, Fast Refresh re-runs effects, which is another reason cleanup must always close the old socket. ## Sending and receiving data A few React Native specifics govern what goes over the wire: - `send` accepts a string, an `ArrayBuffer`, a typed-array view or a `Blob`. Strings go to the native module as text; binary data is base64-encoded across to native code, which costs CPU for large payloads. - Incoming binary frames arrive as an `ArrayBuffer` by default; setting `binaryType = 'blob'` switches to React Native's `Blob`, which is backed by native memory. - Most apps exchange JSON text: `JSON.stringify` on the way out, `JSON.parse` in `onmessage`, with a `type` field so one socket can carry several kinds of message. For an auction, a message shape like `{ type: 'bid', lotId, amount, bidId }` keeps parsing simple and gives every bid an id that later helps with de-duplication. ## Common mistakes - Calling `send` immediately after the constructor, which throws in React Native. - Forgetting `close()` in cleanup, leaving connections that keep receiving bids after the user left. - Logging `onerror`'s event and wondering why the message is empty, instead of reading `onclose`'s `reason`. - Treating every `close` as an error, including the `1000` your own cleanup produced. A good interview answer walks the four events, names the `CONNECTING` trap, shows cleanup, and explains that `close` with its `code` is where failure handling starts.

  • Your onerror handler logs an empty message when the server is down. Where is the actual error?
    React Native 0.87 dispatches a plain `error` event with no details, then a `close` event with code `1006` whose `reason` holds the native failure message. Log and branch in `onclose`, which fires for every ending, not in `onerror`.
  • A user opens three lot screens in a row and the server sees three connections. Why, and what do you change?
    A native stack keeps earlier screens mounted, so each lot screen's effect keeps its own socket open. Either close sockets when a screen loses focus, or move to one app-level connection that screens subscribe to by lot id.

saying these in an interview costs you the question

  • React Native's WebSocket must be imported from a community package.
  • You can call send() right after new WebSocket(); messages are queued.
  • onerror's event contains the reason the connection failed.
  • A screen's socket closes automatically when you push another screen.
  • A close event always means something went wrong.