Online, {{ status().quality }} ({{ status().rtt ?? '?' }} ms)
} @else {Offline
} `, }) export class StatusComponent { status = injectNetworkStatus(watcher) } ``` `status` is a signal, so call it: `status()`. It is read-only, so there is no `set` or `update`. ## Injection context [#injection-context] `injectNetworkStatus` calls `inject(DestroyRef)`, so it must run in an injection context: a constructor, a field initializer (as above), or a function you run with `runInInjectionContext`. Calling it from a method, a lifecycle hook or a timer throws. It subscribes to the watcher when called and unsubscribes through `DestroyRef` when the component (or the injector that owns it) is destroyed. ## Pass your own watcher [#pass-your-own-watcher] The signature is `injectNetworkStatus(watcher?)`. Without an argument it uses `sharedWatcher()` from `@net-watch/core`, which has no `pingUrl`. Without a `pingUrl`, net-watch cannot see "connected but no internet" (see [the ping endpoint guide](/docs/guides/ping-endpoint)). To configure the watcher, create it once in a module and pass it in, as above: ```ts title="network.ts" import { createNetworkWatcher } from '@net-watch/core' export const watcher = createNetworkWatcher({ pingUrl: '/api/ping' }) ``` ## Derived values [#derived-values] Use `computed()` to derive values from the signal: ```ts import { Component, computed } from '@angular/core' import { injectNetworkStatus } from '@net-watch/angular' import { watcher } from './network' @Component({ selector: 'app-quality', template: `{{ label() }}`, }) export class QualityComponent { private status = injectNetworkStatus(watcher) label = computed(() => { const { online, quality, rtt } = this.status() if (!online) return 'Offline' return rtt === undefined ? 'Measuring' : `${quality} (${rtt} ms)` }) } ``` `rtt` is `undefined` until the first sample, and the code shows a placeholder instead of a guess. ## Provide the watcher through DI [#provide-the-watcher-through-di] A module-level `watcher` is the simplest option. If you prefer dependency injection, for example to replace the watcher in tests, provide it through an injection token: ```ts title="network.token.ts" import { InjectionToken } from '@angular/core' import { createNetworkWatcher, type NetworkWatcher } from '@net-watch/core' export const NETWORK_WATCHER = new InjectionToken{{ status().online ? 'Online' : 'Offline' }}
`, }) export class StatusComponent { status = injectNetworkStatus(inject(NETWORK_WATCHER)) } ``` A service works the same way: create the watcher in an `@Injectable({ providedIn: 'root' })` class and pass it to `injectNetworkStatus` from a component. In tests, provide a different value for `NETWORK_WATCHER`. The root watcher lives as long as the app. If you create one with a shorter life, call its `destroy()` yourself. ## SSR [#ssr] On the server, `injectNetworkStatus` reads the fixed server state (`online: true`, `quality: 'unknown'`, metrics `undefined`). In the browser it reads the real state, which on Chromium may already include `rtt` and `quality`. Render quality-dependent UI after the first browser render, and create the banner in `afterNextRender` (a developer preview API before Angular 20). The [SSR guide](/docs/guides/ssr#angular) shows both. ## Banner [#banner] `createBanner` is plain DOM. See [the banner guide](/docs/guides/banner) for options and theming, and [SSR](/docs/guides/ssr#the-banner-needs-the-dom) for an Angular component that creates it safely. ## Next [#next] - [`watchFetch`](/docs/guides/watch-fetch): wrap your requests so the state stays fresh without extra probes. - [State reference](/docs/reference/state): every field and where it is available. # React (/docs/frameworks/react) `@net-watch/react` exports one hook, `useNetworkStatus`. It returns the current `NetworkState` and re-renders your component when the state changes. It needs React 18 or later. ## Install [#install] ```package-install npm i @net-watch/core @net-watch/react ``` ## Minimal component [#minimal-component] ```tsx title="Status.tsx" import { useNetworkStatus } from '@net-watch/react' export function Status() { const { online, quality } = useNetworkStatus() return{online ? `Online, ${quality}` : 'Offline'}
} ``` Called without an argument, the hook uses the shared default watcher. ## Pass your own watcher [#pass-your-own-watcher] The signature is `useNetworkStatus(watcher?)`. The default, `sharedWatcher()` from `@net-watch/core`, is created on first use with default options, which means no `pingUrl`. Without a `pingUrl`, net-watch cannot see "connected but no internet" (see [the ping endpoint guide](/docs/guides/ping-endpoint)). To configure the watcher, create it once in a module and pass it in: ```ts title="network.ts" import { createNetworkWatcher } from '@net-watch/core' export const watcher = createNetworkWatcher({ pingUrl: '/api/ping' }) ``` ```tsx title="Status.tsx" import { useNetworkStatus } from '@net-watch/react' import { watcher } from './network' export function Status() { const { online, quality } = useNetworkStatus(watcher) return{online ? `Online, ${quality}` : 'Offline'}
} ``` Use the same watcher everywhere: pass it to the hook, to `watchFetch` and to `createBanner`. Two watchers would send two sets of probes and could disagree. ## Why useSyncExternalStore [#why-usesyncexternalstore] The hook is a single call to React's `useSyncExternalStore`, which is the built-in way to read a store that lives outside React. It avoids tearing (two components showing different values in one render) and works with concurrent rendering. It needs `getState()` to return the same object until something changes, and net-watch guarantees that: a change replaces the state object, and no change keeps the same reference. Without that guarantee React would see a new value on every read and loop. ### Server snapshot [#server-snapshot] On the server, and during hydration, the hook returns a fixed state: `online: true`, `quality: 'unknown'`, no metrics, `saveData: false`. React then switches to the real state after hydrating, so the server HTML always matches the first client render. See [SSR](/docs/guides/ssr). ## Re-renders [#re-renders] The hook returns the whole state object. A component using it re-renders only when something visible changes: `online`, `quality`, `rtt` (rounded to 50 ms), `downlink` (rounded to 0.1 Mbps below 10 Mbps, to whole Mbps above), `effectiveType`, `type` or `saveData`. A stream of similar latency samples does not cause renders. React has no selector in this hook. If a component reads only `online` and you want it to skip `rtt` updates, keep it small and put it in a leaf component, or subscribe to a single field yourself: ```tsx title="OnlineDot.tsx" import { useSyncExternalStore } from 'react' import { watcher } from './network' export function OnlineDot() { const online = useSyncExternalStore( watcher.subscribe, () => watcher.getState().online, () => true, // server snapshot: the server state is always online ) return {online ? '●' : '○'} } ``` ## A banner component [#a-banner-component] `createBanner` is plain DOM. Create it in an effect and destroy it on cleanup: ```tsx title="OfflineBanner.tsx" import { useEffect } from 'react' import { createBanner } from '@net-watch/core/banner' import { watcher } from './network' export function OfflineBanner() { useEffect(() => { const banner = createBanner(watcher, { slowText: 'Slow connection' }) return () => banner.destroy() }, []) return null } ``` Render it once, near the root. See [the banner guide](/docs/guides/banner) for options and theming. ## Show quality with rtt [#show-quality-with-rtt] `quality` is `'excellent' | 'good' | 'fair' | 'poor' | 'unknown'`, and `rtt` is `undefined` until the first sample. Show a placeholder, and do not guess a value. ```tsx title="ConnectionBadge.tsx" import { useNetworkStatus } from '@net-watch/react' import type { Quality } from '@net-watch/core' import { watcher } from './network' const labels: Record{online ? 'Online' : 'Offline'}
} ``` Server components can render `Online, {{ status.quality }} ({{ status.rtt ?? '?' }} ms)
Offline
``` `status` is a computed ref. In ` ``` See [the banner guide](/docs/guides/banner) for options and theming. ## Nuxt [#nuxt] Create the watcher in a plain module, such as `utils/network.ts`, and import it from components. Importing and creating a watcher on the server is safe: it returns a fixed state (`online: true`, `quality: 'unknown'`) and starts no listeners or timers. On Chromium, the first client render can differ from the server render, because `navigator.connection` may already hold values when the watcher is created. Render quality-dependent UI after mount, or wrap it in `