In a React Native screen, how do you open a WebSocket, handle its onopen, onmessage and onclose events, and close it cleanly?
answer
- a global, no import needed
- send only after onopen
- close in the effect cleanup
- close() defaults to code 1000
- a failure: error, then close 1006
basics
~20 sCreate 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 linesimport { 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
Recall the four events, that send must wait for onopen, and that the socket is created in an effect and closed in its cleanup.
Explain React Native's failure sequence, error then close with 1006 and the reason on the close event, and why send throws while connecting.
Decide between screen-scoped and app-scoped connections given stack navigators keeping screens mounted, and treat onclose as the single place for failure handling.
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.