A review of one of the wallpaper apps I run once said, "I opened it where there was no Wi-Fi and my data melted in seconds." The cause was immediate: the list screen was aggressively prefetching the next page of high-resolution images. Behavior that adds comfort on Wi-Fi was quietly eating the user's mobile data on a metered connection. The more images an app handles, the more this "well-intentioned prefetch" backfires.
This walkthrough, using a Rork-generated Expo app, lays out a network design that reads the connection's character and adapts prefetch and image quality. The key is to stop seeing the connection as a binary "connected or not," and go further: is it metered, is it slow, is it a saver mode?
Stop judging on "online or not" alone
Many apps look only at isConnected and, if connected, do everything. But users' connections are not uniform. NetInfo's details carries far more you can act on.
| Signal | Source (NetInfo) | What it means |
|---|---|---|
| Metered? | details.isConnectionExpensive | OS considers it a "costly" connection (includes tethering) |
| Connection type | type (wifi / cellular / ...) | Wi-Fi or mobile |
| Cellular generation | details.cellularGeneration (3g/4g/5g) | A speed proxy (avoid heavy prefetch on 3g) |
| Saver mode | see below | The user is explicitly conserving data |
isConnectionExpensive is the OS's "metered-equivalent" flag, and it includes Wi-Fi over tethering. Honoring just this prevents most "data melts away" incidents like the one in that review.
Low Data Mode needs a caveat. There is no clean, unified API for an app to read iOS Low Data Mode or Android Data Saver as a simple on/off. In practice, treat isConnectionExpensive as a proxy for saver intent, and pair it with a second layer: an in-app conservation switch the user controls. Merge both the OS's intent (metered judgment) and the app's setting (explicit switch) into the policy.
Centralize the network judgment
Scatter the connection judgment across screens and the decisions will inevitably diverge. Build one layer that converts connection info into a single policy, and have every screen look only at that policy.
// net/networkPolicy.ts
import NetInfo, { NetInfoState } from "@react-native-community/netinfo";
export type PrefetchLevel = "full" | "lite" | "off";
export type ImageQuality = "high" | "medium" | "low";
export interface NetworkPolicy {
prefetch: PrefetchLevel; // strength of look-ahead
imageQuality: ImageQuality; // quality to fetch
allowAutoplay: boolean; // whether video/GIF autoplay is allowed
}
// Convert connection state + the app's saver setting into one policy
export function derivePolicy(
state: NetInfoState,
saverEnabled: boolean
): NetworkPolicy {
// Not connected
if (!state.isConnected) {
return { prefetch: "off", imageQuality: "low", allowAutoplay: false };
}
const expensive = !!(state.details as any)?.isConnectionExpensive;
const gen = (state.details as any)?.cellularGeneration as string | undefined;
const isSlowCellular = state.type === "cellular" && (gen === "2g" || gen === "3g");
// User chose to conserve, OR OS says metered, OR it is slow
if (saverEnabled || expensive || isSlowCellular) {
return { prefetch: "off", imageQuality: "medium", allowAutoplay: false };
}
// Ordinary mobile (4g/5g, non-metered)
if (state.type === "cellular") {
return { prefetch: "lite", imageQuality: "high", allowAutoplay: false };
}
// Wi-Fi (non-metered) — prefetch at full strength
return { prefetch: "full", imageQuality: "high", allowAutoplay: true };
}Hand this policy out through a React context and every screen makes a consistent decision just by reading useNetworkPolicy().
// net/NetworkPolicyProvider.tsx
import React, { createContext, useContext, useEffect, useState } from "react";
import NetInfo from "@react-native-community/netinfo";
import { derivePolicy, NetworkPolicy } from "./networkPolicy";
import { useDataSaver } from "../settings/useDataSaver"; // in-app conservation switch
const Ctx = createContext<NetworkPolicy>({
prefetch: "lite", imageQuality: "high", allowAutoplay: false,
});
export function NetworkPolicyProvider({ children }: { children: React.ReactNode }) {
const saver = useDataSaver();
const [policy, setPolicy] = useState<NetworkPolicy>(Ctx._currentValue);
useEffect(() => {
// Fetch the current value once on subscribe, then listen for changes
const unsub = NetInfo.addEventListener((state) => {
setPolicy(derivePolicy(state, saver));
});
NetInfo.fetch().then((state) => setPolicy(derivePolicy(state, saver)));
return () => unsub();
}, [saver]);
return <Ctx.Provider value={policy}>{children}</Ctx.Provider>;
}
export const useNetworkPolicy = () => useContext(Ctx);The key is including the saver switch (saver) in the dependencies. The moment the user flips the switch in settings, the policy recomputes and prefetch behavior follows instantly.