# Connection

> **Info:** This is release candidate documentation for **Stream Chat React SDK v15**. For the latest stable version, see the [latest version (v14)](https://getstream.io/chat/docs/sdk/react/).

A chat connection can fail in two ways. The device can lose its network, or the client's WebSocket can drop while the network is fine: the server closed it, the token expired, or a health check timed out. The client reports the two separately, so an app can tell "you're offline" from "reconnecting".

When the socket comes back, the client brings what the user is looking at up to date by itself, then announces it with `connection.recovered`.

This page covers both: the connection status, then recovery. Showing a lost-connection banner is on [Connection status](https://getstream.io/chat/docs/sdk/react/v15/features/app-experience/connection-status/), and reading these stores in components on [Reading state in components](https://getstream.io/chat/docs/sdk/react/v15/ui/reading-state-in-components/).

## Connection status

| Question                                  | Where to read it                             |
| ----------------------------------------- | -------------------------------------------- |
| Is the WebSocket up?                      | `client.wsConnection.state`, `isHealthy`     |
| Does the device have a network?           | `client.networkConnection.state`, `isOnline` |
| Has the client caught up after reconnect? | the `connection.recovered` event             |

The two stores can disagree either way. A socket can drop on a working network. And when a device loses its network without a reporter telling the client, the socket can look healthy for up to 35 seconds, until its next health check fails. A reported loss marks the socket down at once.

The status isn't an event: subscribe to the store you mean.

### Socket health

`client.wsConnection.state` holds `{ isHealthy, lastHealthyAt, lastUnhealthyAt }`. `isHealthy` is always a boolean, so `!isHealthy` is safe. `client.wsConnection.isHealthy` reads the current value.

The store records every change, including a deliberate `client.closeConnection()`, and it records a drop the moment it happens. Most drops recover in under a second, so a UI that shows every drop at once makes a working app look broken. Wait `offlineNotificationDisplayDelayMs` before showing one, and cancel the wait if the socket comes back:

```ts
const delayMs = client.wsConnection.config.offlineNotificationDisplayDelayMs;
let timer: ReturnType<typeof setTimeout> | undefined;

const unsubscribe = client.wsConnection.state.subscribeWithSelector(
  ({ isHealthy }) => ({ isHealthy }),
  ({ isHealthy }) => {
    clearTimeout(timer);
    if (isHealthy) {
      console.log("Connected");
      return;
    }
    timer = setTimeout(() => console.log("Reconnecting…"), delayMs);
  },
);
```

The socket's timings are set through `client.config`, which [Configuration](https://getstream.io/chat/docs/sdk/react/v15/state/state-and-entities/configuration/) explains:

```ts
client.config.set({
  client: {
    wsConnection: {
      connectTimeoutMs: 15_000, // how long connecting waits for the server
      healthCheckGracePeriodMs: 10_000, // extra time before a silent socket counts as dead
      offlineNotificationDisplayDelayMs: 5_000, // how long a UI waits before showing a drop
      pingIntervalMs: 25_000, // how often a health check goes out
    },
  },
});
```

The values above are the defaults:

- `pingIntervalMs` can only be lowered: 25 seconds is also the maximum, because a slower ping risks the server closing the connection as idle.
- The socket counts as dead after `pingIntervalMs + healthCheckGracePeriodMs` without an answer, 35 seconds by default. Lowering the ping makes a dead socket noticed sooner.
- `offlineNotificationDisplayDelayMs` is the one setting the client doesn't act on. It's there so every UI waits the same time.
- `enableWSFallback: true` falls back to HTTP long-polling when the WebSocket can't connect, for networks that block WebSockets. The store then follows the long-poll.

### Device network

`client.networkConnection.state` holds `{ isOnline, lastOnlineAt, lastOfflineAt }`, and `client.networkConnection.isOnline` reads the current value.

`isOnline` is `undefined` when the status is unknown: nothing has reported it yet. Test it with `=== false`:

```ts
if (client.networkConnection.isOnline === false) {
  // The device is offline.
}

// Wrong: also true while the status is unknown, such as before the first connection.
if (!client.networkConnection.isOnline) {
  // ...
}
```

The client can't detect the network by itself: every platform reports it differently. So a **reporter** tells it. A reporter is a function that reports the current status, reports again on every change, and returns a function that stops it.

- **In a browser**, the client installs one for you, based on `navigator.onLine` and the `online` and `offline` events.
- **Elsewhere**, such as in React Native, install your own. Until you do, the client copies the socket's status, which can't tell a dead socket from a lost network.

```ts
import type { NetworkStatusReporter } from "stream-chat";

const reporter: NetworkStatusReporter = (onStatusChange) => {
  // Report the current status right away, then on every change.
  onStatusChange(navigator.onLine);

  const handle = () => onStatusChange(navigator.onLine);
  window.addEventListener("online", handle);
  window.addEventListener("offline", handle);

  return () => {
    window.removeEventListener("online", handle);
    window.removeEventListener("offline", handle);
  };
};

client.config.set({
  client: { networkConnection: { statusReporter: reporter } },
});
// or, after the client exists:
client.networkConnection.setStatusReporter(reporter);
```

The reporter set last wins, and both ways survive later `client.config.set()` calls. Setting the same function again does nothing. If the reporter throws, the status stays unknown. A host with no event to listen to can report the status directly with `client.networkConnection.setStatus(isOnline)`.

Don't hold back requests until the network reports online. Everything works while the status is unknown, and the client doesn't need it: it only helps notice a drop, and a return, sooner.

### Closing and reopening the socket on mobile

A mobile app receives push notifications only while it has no open socket. So when the app goes to the background, close the socket, and reopen it when the app comes back:

```ts
// The app went to the background.
await client.closeConnection();

// The app is back in the foreground.
await client.openConnection();
```

`client.closeConnection()` keeps the user connected to the client, and doesn't reconnect by itself. The socket store records the close as a drop. Reopening counts as a reconnect, so recovery runs, as below.

## Connection recovery

### What reloads after a reconnect

>
> **Info:** **Example**
>
> The user's laptop sleeps for ten minutes. While it sleeps, they miss messages, reactions and read receipts, and the server ends every watch their client held. When the socket comes back, the client:
>
> 1. **Refreshes every channel list that has loaded.** Each list queries again, page by page, the channels it has loaded, up to 150, and keeps them on screen meanwhile. Then it removes the loaded channels no page returned: channels that were deleted, that the user left, or that were hidden, archived or stopped matching the list's filters while they were away. Channels loaded beyond the first 150 are dropped, and load again when the list is scrolled to them. These requests watch their channels again.
> 2. **Reloads every active channel.** An active channel is one something opened with `Channel.activate()`. The channel watches again and refreshes its newest loaded interval of messages, not the older intervals a jump loaded: messages deleted there while the user was away disappear, and their unsent messages stay.
> 3. **Reloads every active thread's replies.**
> 4. **Marks every other thread the client holds as stale.** A stale thread loads again the next time it's opened.
> 5. **Dispatches `connection.recovered`** once the active channels and threads have reloaded. The channel lists refresh alongside them, and can finish later.
>

After that event, the thread list loads again, if it was opened in this session.

With offline support, steps 1 and 2 wait until the changes the user made while offline have been sent. A first connection isn't a reconnect, so none of this runs then. The UI layer activates the channels and threads it shows, so they're the ones that reload. [Entity lifecycle](https://getstream.io/chat/docs/sdk/react/v15/state/state-and-entities/entity-lifecycle/) explains `Channel.activate()` and `Thread.activate()`.

### Channels re-watched on demand

A watch is what makes the server send a channel's events to this client. The server ties watches to the connection, so a reconnect ends them all. The steps above watch again the channels each list loaded again and the active channels.

Other channels the user's client holds keep `watchStatus` set to `wasWatching`: channels a list had loaded beyond the first 150, channels they opened earlier, channels no list shows. Watching them all again could exceed the server's watch limit. Instead, each one is watched again when the channel manager routes it after an event, for example `notification.message_new` when someone writes in it.

### connection.recovered

`connection.recovered` fires after every reconnect: an automatic one, and `client.closeConnection()` followed by `client.openConnection()`. It carries nothing but its type.

By the time it fires, the client has reloaded its active channels and threads, and its channel lists are refreshing. Listen for it to refresh what the client doesn't own: results of one-off queries, counters, your own data.

For example, an app that shows the user's total unread count from a one-off query loads it again:

```ts
let totalUnread = 0;

const loadTotalUnread = async () => {
  const { total_unread_count } = await client.unreadCounts();
  totalUnread = total_unread_count;
};

const { unsubscribe } = client.on("connection.recovered", () => {
  void loadTotalUnread();
});
```

If the socket drops again while the client reloads, the event waits for the next reconnect, whose recovery fires it.

### Turning recovery off or running it by hand

An app that brings its data up to date itself can turn recovery off:

```ts
client.config.set({ client: { connectionRecovery: { enabled: false } } });
```

Then a reconnect refreshes no channel list, reloads no channel or thread, and marks no thread stale. `connection.recovered` still fires after every reconnect, so the app can run its own recovery when it arrives. The setting is read at each reconnect, so a change applies from the next one.

To run a recovery yourself, call `client.connectionRecovery.recover()`. It does steps 1 to 5 above, whether or not recovery runs on its own. It does nothing while a recovery is already running.

## Used in

- [Connection status](https://getstream.io/chat/docs/sdk/react/v15/features/app-experience/connection-status/)
- [Notifications](https://getstream.io/chat/docs/sdk/react/v15/features/app-experience/notifications/)
- [System notification banner](https://getstream.io/chat/docs/sdk/react/v15/recipes/app/system-notification-banner/)

---

For the most recent version of this documentation, visit [https://getstream.io/chat/docs/sdk/react/v15/state/connection/](https://getstream.io/chat/docs/sdk/react/v15/state/connection/).