1. Detecting Connectivity
npx expo install @react-native-community/netinfo
import { useEffect, useState } from 'react';
import NetInfo from '@react-native-community/netinfo';
export function useIsOnline() {
const [isOnline, setIsOnline] = useState(true);
useEffect(() => {
const unsubscribe = NetInfo.addEventListener((state) => {
setIsOnline(Boolean(state.isConnected && state.isInternetReachable));
});
return unsubscribe;
}, []);
return isOnline;
}
isConnected means the device has a network interface up (Wi-Fi or
cellular); isInternetReachable means that connection actually reaches
the internet — a phone connected to a Wi-Fi network with no internet access reports
isConnected: true but isInternetReachable: false.
2. Queueing Mutations While Offline
Rather than letting a failed request vanish, write it to a small persisted queue (SQLite, from Week 9, is a natural fit) and attempt it immediately only if online.
type QueuedMutation = { id: string; type: 'createPost'; payload: unknown };
export async function enqueueMutation(mutation: QueuedMutation) {
db.runSync(
'INSERT INTO sync_queue (id, type, payload) VALUES (?, ?, ?);',
[mutation.id, mutation.type, JSON.stringify(mutation.payload)]
);
}
export async function trySubmit(mutation: QueuedMutation, isOnline: boolean) {
await enqueueMutation(mutation); // always persist first
if (isOnline) {
await flushQueue(); // attempt immediately if we can
}
}
Persisting before attempting the request means a mutation the user made right as connectivity drops isn't lost — it's already safely queued regardless of whether the immediate attempt succeeds or fails.
3. Syncing on Reconnect
export async function flushQueue() {
const pending = db.getAllSync('SELECT * FROM sync_queue ORDER BY id ASC;');
for (const item of pending) {
try {
await apiFetch('/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: item.payload,
});
db.runSync('DELETE FROM sync_queue WHERE id = ?;', [item.id]);
} catch {
break; // stop on first failure, retry the rest next time
}
}
}
useEffect(() => {
return NetInfo.addEventListener((state) => {
if (state.isConnected && state.isInternetReachable) {
flushQueue();
}
});
}, []);
Processing the queue in order, and stopping on the first failure rather than skipping it, keeps writes applied in the sequence the user made them — important when a later mutation depends on an earlier one having already landed.
4. A Basic Conflict Strategy
Once two devices (or a device and a web client) can both edit the same record while one was offline, they can disagree about its current state. Full conflict resolution is a deep topic; two simple, honest strategies cover most apps starting out:
- Last-write-wins — the server simply accepts whichever update arrives last, timestamped. Simple, occasionally loses a change, acceptable for low-stakes data like a draft note.
- Reject & surface — the server detects the record changed since the client last saw it (a version number or
updated_atcheck) and returns a conflict error the app shows to the user, rather than silently overwriting.
Start with whichever matches how bad it would actually be for a user's change to silently disappear — for a cart, last-write-wins is usually fine; for anything financial, reject-and-surface is the safer default.
5. Hands-on Exercise
Make an earlier screen work offline
Take the create-post flow from Week 10 and make it resilient to going offline mid-use.
Requirements:
- A
useIsOnlinehook, and a persistent banner shown at the top of the app whenever offline. - The "create post" mutation from Week 10 rewritten to always enqueue locally first, in a SQLite table, before attempting the network call.
- An automatic flush triggered by a NetInfo listener when connectivity returns, processing queued items in order.
- A visible "pending sync" indicator on any post created while offline, cleared once it successfully syncs.
- Manually test it: turn on airplane mode, create two posts, turn it back off, and confirm both sync in the order they were created.
If flushQueue seems to run but nothing actually clears, confirm the DELETE in your queue table is matching on the right column — a queued item's local id is not the same as the server's ID for that record once it's created, so deleting "by the wrong ID" silently no-ops.
6. Knowledge Check
Four quick questions. Expand each to check your answer.
Q1
Why can isConnected be true while isInternetReachable is false?
Why can isConnected be true while isInternetReachable is false?
isConnected only reports whether a network interface (Wi-Fi, cellular) is active — it says nothing about whether that network actually reaches the internet. A device on a Wi-Fi network with no internet access (a captive portal, a router with no uplink) is a real, common case where both differ.
Q2
Why persist a mutation to the local queue before attempting the network request, rather than only queueing it after the request fails?
Why persist a mutation to the local queue before attempting the network request, rather than only queueing it after the request fails?
Connectivity can drop in the instant between deciding to send a request and it completing — persisting first guarantees the mutation is safely recorded regardless of whether the immediate attempt happens to succeed, fail, or never gets a chance to run at all.
Q3
Why does flushQueue stop processing on the first failure instead of skipping it and trying the next item?
Why does flushQueue stop processing on the first failure instead of skipping it and trying the next item?
If mutations must apply in the order the user made them (a common requirement — e.g. an edit that depends on a prior create having landed), skipping a failed item and continuing risks applying later mutations out of order or against a record that doesn't exist yet on the server. Stopping and retrying from the same point next time preserves ordering.
Q4
For a shopping cart, why might last-write-wins be an acceptable conflict strategy where it wouldn't be for a bank balance?
For a shopping cart, why might last-write-wins be an acceptable conflict strategy where it wouldn't be for a bank balance?
Losing a cart update to a conflicting write is a minor, easily-recoverable annoyance — the user just re-adds the item. For financial data, silently discarding a conflicting write could mean money is unaccounted for, which is a correctness and trust problem serious enough to justify surfacing the conflict to the user instead of resolving it silently.