All files / hooks/use-network-state/src useNetworkState.ts

92.85% Statements 26/28
83.33% Branches 5/6
75% Functions 6/8
92.85% Lines 26/28

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124                                                                                                                                                18x         18x   18x 11x       11x 3x 3x     11x 4x     11x 11x   11x 11x   11x 11x 11x 11x       18x 60x 60x 60x 43x   17x 17x     18x         18x    
import { useCallback, useRef, useSyncExternalStore } from "react";
import type { NetworkState, UseNetworkStateReturn } from "./types";
import {
  SERVER_NETWORK_STATE,
  areNetworkStatesEqual,
  getConnection,
  getNetworkState,
} from "./utils";
 
/**
 * Track the device's network status: online/offline connectivity plus the
 * Network Information API (`effectiveType`, `downlink`, `rtt`, `saveData`, …).
 *
 * Combines `navigator.onLine` (updated on the window `online`/`offline` events)
 * with `navigator.connection` (updated on its `change` event, with
 * `mozConnection`/`webkitConnection` fallbacks). Built on
 * `useSyncExternalStore`, so it is tear-free under concurrent rendering and
 * SSR-safe out of the box.
 *
 * Degrades gracefully: on browsers without the Network Information API
 * (Firefox, Safari) every Network Information field is `undefined` while
 * `online` still works. On the server it returns a deterministic inert value
 * with `online: true` and no Network Information fields, so it never throws and
 * never causes a hydration mismatch.
 *
 * `since` is the timestamp of the *last online/offline transition* — it is
 * `undefined` on the initial render and until the first transition occurs.
 *
 * @returns The current {@link NetworkState}.
 *
 * @example
 * ```tsx
 * // Basic online/offline banner
 * function ConnectivityBanner() {
 *   const { online } = useNetworkState();
 *   if (online) return null;
 *   return <div role="alert">You are offline.</div>;
 * }
 * ```
 *
 * @example
 * ```tsx
 * // Adapt behaviour to connection quality
 * function Media() {
 *   const { effectiveType, saveData } = useNetworkState();
 *   const lowData = saveData || effectiveType === "slow-2g" || effectiveType === "2g";
 *   return lowData ? <LowResImage /> : <HighResImage />;
 * }
 * ```
 *
 * @example
 * ```tsx
 * // Full snapshot
 * function NetworkInfo() {
 *   const { online, since, downlink, effectiveType, rtt, type } = useNetworkState();
 *   return (
 *     <ul>
 *       <li>online: {String(online)}</li>
 *       <li>since: {since?.toLocaleTimeString() ?? "—"}</li>
 *       <li>effectiveType: {effectiveType ?? "unsupported"}</li>
 *       <li>downlink: {downlink ?? "—"} Mb/s</li>
 *       <li>rtt: {rtt ?? "—"} ms</li>
 *       <li>type: {type ?? "unsupported"}</li>
 *     </ul>
 *   );
 * }
 * ```
 */
export function useNetworkState(): UseNetworkStateReturn {
  // Timestamp of the last online/offline transition. Mutated by the event
  // handlers in `subscribe` and read by `getSnapshot`. Starts undefined so the
  // first render reports no transition yet.
  const sinceRef = useRef<Date | undefined>(undefined);
 
  // Cache the last snapshot so `getSnapshot` can return a stable reference when
  // nothing observable changed. `useSyncExternalStore` compares snapshots with
  // Object.is and would loop forever if we returned a fresh object every call.
  const cacheRef = useRef<NetworkState | undefined>(undefined);
 
  const subscribe = useCallback((onStoreChange: () => void) => {
    Iif (typeof window === "undefined") {
      return () => {};
    }
 
    const handleOnlineStatusChange = () => {
      sinceRef.current = new Date();
      onStoreChange();
    };
 
    const handleConnectionChange = () => {
      onStoreChange();
    };
 
    window.addEventListener("online", handleOnlineStatusChange);
    window.addEventListener("offline", handleOnlineStatusChange);
 
    const connection = getConnection();
    connection?.addEventListener?.("change", handleConnectionChange);
 
    return () => {
      window.removeEventListener("online", handleOnlineStatusChange);
      window.removeEventListener("offline", handleOnlineStatusChange);
      connection?.removeEventListener?.("change", handleConnectionChange);
    };
  }, []);
 
  const getSnapshot = useCallback((): NetworkState => {
    const next = getNetworkState(sinceRef.current);
    const prev = cacheRef.current;
    if (prev && areNetworkStatesEqual(prev, next)) {
      return prev;
    }
    cacheRef.current = next;
    return next;
  }, []);
 
  const getServerSnapshot = useCallback(
    (): NetworkState => SERVER_NETWORK_STATE,
    []
  );
 
  return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
}