skip to content

In an Expo app, how does expo-network's useNetworkState differ from NetInfo's useNetInfo, and when is it not enough?

level: middleimportance: should knowfreq 24%

answer

  1. an Expo SDK module, not a community one
  2. starts as an empty object
  3. uppercase NetworkStateType values
  4. iOS reachable mirrors connected
  5. no configurable probe

basics

~20 s

expo-network's useNetworkState returns { type, isConnected, isInternetReachable }, starting as an empty object. On Android reachability comes from the OS's validated-network check, but on iOS isInternetReachable always equals isConnected, and there is no probe to configure; NetInfo covers those gaps.

solid answer

~40 s

`useNetworkState()` from `expo-network` calls `getNetworkStateAsync()` and subscribes with `addNetworkStateListener`, returning a `NetworkState` with optional `type`, `isConnected` and `isInternetReachable`. Its initial value is `{}`, so all three are `undefined` until the first read resolves. `type` uses an uppercase enum, `NetworkStateType.WIFI`, `CELLULAR`, `NONE` and so on, unlike NetInfo's lowercase strings. On Android `isInternetReachable` requires the internet and validated capabilities on the active network, but on iOS the docs state it is always the same as `isConnected`, so a captive portal looks online. There is no `reachabilityUrl`, no probe cadence and no cost flag. It is fine for a simple indicator in an Expo app; when the field-inspection banner must catch captive portals on iOS or defer uploads on metered networks, NetInfo is the better tool.

code

tsx · 10 lines
tsx
import { Text } from 'react-native';
import { NetworkStateType, useNetworkState } from 'expo-network';

export function ConnectionLabel() {
  const { type, isInternetReachable } = useNetworkState();

  if (type === undefined) return null; // first render: {}
  if (isInternetReachable === false) return <Text>Offline</Text>;
  return <Text>{type === NetworkStateType.WIFI ? 'On Wi-Fi' : 'Online'}</Text>;
}

go deeper

for a junior

Recall that expo-network's useNetworkState returns type, isConnected and isInternetReachable, and that the first render has no values yet.

for a middle

Explain the uppercase enum, the empty initial state and that on iOS expo-network's reachability simply mirrors connectivity.

for a senior

Choose between expo-network and NetInfo per requirement, captive-portal detection, cost signals and probe control, and wrap the choice in one module.

for a principal

Standardise one connectivity source across the app and its teams, so every feature reads the same definition of online and offline.

## Two libraries for one question An Expo project can read network status with either: - **`expo-network`** – an Expo SDK module (version 57.x in SDK 57) exposing `getNetworkStateAsync()`, `addNetworkStateListener()`, the `useNetworkState()` hook, plus extras such as `getIpAddressAsync()` and, on Android, `isAirplaneModeEnabledAsync()`. - **`@react-native-community/netinfo`** – the community library with `fetch`, `refresh`, `addEventListener`, `useNetInfo` and a configurable reachability probe. Both work in Expo apps. The differences decide which one a screen should use. ## What useNetworkState returns `useNetworkState()` initialises its state to an **empty object**, calls `getNetworkStateAsync()` and subscribes to changes, removing the listener on unmount. The returned `NetworkState` has three **optional** fields: 1. `type` – a `NetworkStateType` enum value: `NONE`, `UNKNOWN`, `CELLULAR`, `WIFI`, `BLUETOOTH`, `ETHERNET`, `WIMAX`, `VPN`, `OTHER`. These are **uppercase strings**, unlike NetInfo's `'wifi'`. 2. `isConnected` – `false` for `NONE` and `UNKNOWN`, `true` otherwise. 3. `isInternetReachable` – see below. On the first render all three are `undefined`, so `state.isInternetReachable === false` is the safe offline test here too. ## The iOS caveat The fields look alike, but the semantics differ by platform: | | expo-network | NetInfo 12 | |---|---|---| | Android reachability | Internet + validated capabilities (VPN also needs bandwidth) | OS validated-network check | | iOS reachability | **Always equal to `isConnected`** | HTTP probe to `reachabilityUrl` | | Configurable probe | No | Yes (URL, method, test, cadence) | | Cost signal | No | `details.isConnectionExpensive` | | Initial hook value | `{}` (fields `undefined`) | `type: 'unknown'`, booleans `null` | | Type values | `NetworkStateType.WIFI` | `'wifi'` | On iOS, therefore, `expo-network` cannot tell "joined the hotel Wi-Fi" from "online". For many apps that is acceptable; for an offline banner that must be right behind captive portals, it is not. ## Choosing - **Use `expo-network`** for a lightweight indicator, a one-off `getNetworkStateAsync()` check, or when you also need the IP address or Android's airplane-mode check. - **Use NetInfo** when you need real reachability on iOS, a probe against your own API, a cost signal for deferring uploads, or cached-versus-fresh control through `fetch` and `refresh`. - **Do not mix them for one decision.** Two sources with different semantics will disagree, and the UI will flicker between them. Pick one per app, wrap it in a small module, and have screens read from that. ## Migrating between them Teams often start with `expo-network` because it ships with the SDK, then switch when a captive-portal bug report arrives from iOS users. The migration is mechanical if network state already sits behind one module: 1. Replace `useNetworkState()` with `useNetInfo()` inside that module. 2. Map the uppercase enum to NetInfo's lowercase `type` strings, or expose your own small union type so screens never see either library's values. 3. Replace `undefined` checks with `null` checks for the unknown state. 4. Call `NetInfo.configure` once at startup if you want a custom probe. 5. Remove `expo-network` if nothing else, such as the IP address lookup, still needs it. Screens that imported either library directly each need the same edits, which is the main argument for the wrapper module in the first place. ## Pitfalls - Comparing `type === 'wifi'` against `expo-network` values, which never matches because the enum is uppercase. - Treating `undefined` on the first render as offline. - Assuming the iOS `isInternetReachable` from `expo-network` detects captive portals. - Forgetting to remove a listener created with `addNetworkStateListener` outside the hook; it returns a subscription with `remove()`. ## Interview framing State the API shape, the empty initial value, the uppercase enum and, above all, the **iOS reachability equals connectivity** caveat, then say when you would reach for NetInfo instead. That shows you read beyond the hook's name.

  • An Expo app's banner never shows 'Offline' on an iPhone stuck behind a hotel login page. Why?
    With `expo-network`, `isInternetReachable` on iOS always equals `isConnected`, and the phone is connected to Wi-Fi, so both are true. Detecting the captive portal on iOS needs an actual request, such as NetInfo's reachability probe or a failed call to your own API.
  • How do you unsubscribe from expo-network's addNetworkStateListener outside a component?
    It returns an event subscription object; call its `remove()` method when the service stops. The `useNetworkState` hook does exactly that in its effect cleanup.

saying these in an interview costs you the question

  • expo-network's isInternetReachable on iOS detects captive portals.
  • useNetworkState returns the real network state on its first render.
  • expo-network's type values are lowercase strings like 'wifi'.
  • expo-network lets you set a custom reachability URL.
  • Using expo-network and NetInfo side by side for the same banner is fine.