Network Connectivity
Modern web apps need to handle network failures gracefully. Users go offline, connections drop, and APIs become unavailable. Storion provides a network module that makes your app resilient to these issues.
The Problem
Network failures in traditional React apps often result in:
- Silent failures with no user feedback
- Stuck loading states
- Lost data when requests fail
- No automatic recovery when connection returns
Storion's network module provides:
- Real-time connectivity status — Know when you're online/offline
- Automatic retry — Retry failed requests with configurable strategies
- Network-aware waiting — Pause operations until connection returns
- Customizable detection — Override for React Native or custom ping logic
Installation
import { networkStore, networkService } from "storion/network";Checking Network Status
In Components
Display connectivity status to users:
function NetworkBanner() {
const { online } = useStore(({ get }) => {
const [state] = get(networkStore);
return { online: state.online };
});
if (online) return null;
return (
<div className="offline-banner">
<span>⚠️</span> You are offline. Some features may be unavailable.
</div>
);
}In Store Setup
Check connectivity before making requests:
import { networkService } from "storion/network";
const dataStore = store({
name: "data",
state: { items: async.fresh<Item[]>() },
setup({ get, focus }) {
const network = get(networkService);
const itemsQuery = async.action(focus("items"), async (ctx) => {
// Early exit if offline
if (!network.isOnline()) {
throw new Error("No network connection");
}
const res = await fetch("/api/items", { signal: ctx.signal });
return res.json();
});
return { fetchItems: itemsQuery.dispatch };
},
});Waiting for Connectivity
Use networkService.waitForOnline() to pause execution until the network is available:
import { networkService } from "storion/network";
const syncStore = store({
name: "sync",
state: { pendingChanges: [] },
setup({ state, get }) {
const network = get(networkService);
return {
syncPendingChanges: async () => {
// Wait until we're online
await network.waitForOnline();
// Now safe to sync
for (const change of state.pendingChanges) {
await uploadChange(change);
}
state.pendingChanges = [];
},
};
},
});Use cases:
- Background sync when connection returns
- Queuing operations while offline
- Ensuring critical requests wait for connectivity
Network-Aware Retry
The offlineRetry() Wrapper
The networkService.offlineRetry() wrapper automatically waits for network reconnection before retrying:
import { abortable, retry } from "storion/async";
import { networkService } from "storion/network";
setup({ get, focus }) {
const network = get(networkService);
// Define your fetch function
const fetchData = abortable(async ({ signal }) => {
const res = await fetch("/api/data", { signal });
return res.json();
});
// Chain wrappers: retry, then wait for network
const robustFetch = fetchData
.use(retry(3)) // Retry up to 3 times
.use(network.offlineRetry()); // Wait for network on failure
const dataQuery = async.action(focus("data"), robustFetch);
return { fetchData: dataQuery.dispatch };
}How it works:
- Request fails
retry(3)retries up to 3 times with backoff- If still failing AND device is offline:
offlineRetry()waits for network reconnection- Retries once after reconnection
- If device is online, error is thrown immediately (it's not a network issue)
Retry Strategies
Storion provides built-in delay strategies:
| Strategy | Delays | Best for |
|---|---|---|
backoff | 1s, 2s, 4s, 8s... (max 30s) | Most API calls |
linear | 1s, 2s, 3s, 4s... (max 30s) | Gradual increase |
fixed | 1s, 1s, 1s... | Consistent intervals |
fibonacci | 1s, 1s, 2s, 3s, 5s, 8s... | Moderate backoff |
immediate | 0, 0, 0... | Quick retries (use carefully) |
import { retry, retryStrategy } from "storion/async";
// Use named strategy
const robustFetch = fetchData.use(retry("backoff"));
// Custom retry options
const robustFetch = fetchData.use(
retry({
retries: 5,
delay: "fibonacci",
})
);
// Add jitter to prevent thundering herd
const robustFetch = fetchData.use(
retry({
retries: 3,
delay: retryStrategy.withJitter(retryStrategy.backoff),
})
);Combining Retry Strategies
Order matters when chaining wrappers:
const network = get(networkService);
// Recommended order: retry first, then network awareness
const robustFetch = fetchData
.use(retry(3)) // 1. Retry transient errors
.use(network.offlineRetry()); // 2. Wait for network if still failing
// The chain handles:
// - Transient server errors (retry handles these)
// - Network disconnection (offlineRetry waits and retries)Customizing Network Detection
Custom Ping Logic
By default, Storion uses navigator.onLine. This can be unreliable (it only checks if there's a network interface, not actual connectivity). Override with custom ping:
import { container } from "storion";
import { pingService } from "storion/network";
const app = container();
app.set(pingService, () => ({
ping: async () => {
try {
const res = await fetch("/api/health", {
method: "HEAD",
cache: "no-store",
});
return res.ok;
} catch {
return false;
}
},
}));React Native Support
React Native doesn't have navigator.onLine. Override onlineService:
import NetInfo from "@react-native-community/netinfo";
import { container } from "storion";
import { onlineService } from "storion/network";
const app = container();
app.set(onlineService, () => ({
isOnline: () => true, // Optimistic initial value
subscribe: (listener) =>
NetInfo.addEventListener((state) => {
listener(!!state.isConnected);
}),
}));Complete Example
Here's a full example showing all network features working together:
import { store, useStore, trigger } from "storion/react";
import { async, abortable, retry, timeout } from "storion/async";
import { networkStore, networkService } from "storion/network";
// API function with signal support
const fetchUsers = abortable(async ({ signal }) => {
const res = await fetch("/api/users", { signal });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json() as Promise<User[]>;
});
// Store with network-aware async
const userStore = store({
name: "users",
state: {
users: async.stale<User[]>([]),
},
setup({ get, focus }) {
const network = get(networkService);
// Build robust fetch with all wrappers
const robustFetch = fetchUsers
.use(timeout(10000)) // 10s timeout
.use(retry(3)) // Retry 3 times
.use(network.offlineRetry()); // Wait for network
const usersQuery = async.action(focus("users"), robustFetch);
return {
fetchUsers: usersQuery.dispatch,
refresh: usersQuery.refresh,
};
},
});
// Component with network awareness
function UserList() {
const { users, online, fetchUsers } = useStore(({ get }) => {
const [networkState] = get(networkStore);
const [userState, userActions] = get(userStore);
// Auto-fetch on mount
trigger(userActions.fetchUsers, []);
return {
users: userState.users,
online: networkState.online,
fetchUsers: userActions.fetchUsers,
};
});
return (
<div>
{/* Offline indicator */}
{!online && (
<div className="warning">
⚠️ You are offline. Showing cached data.
</div>
)}
{/* Loading state */}
{users.status === "pending" && <Spinner />}
{/* Error state with retry */}
{users.status === "error" && (
<div className="error">
Failed to load users: {users.error.message}
<button onClick={() => fetchUsers()}>Retry</button>
</div>
)}
{/* User list (works in stale mode even while loading) */}
<ul>
{users.data.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
</div>
);
}Best Practices
1. Use Stale Mode for Lists
Keep showing previous data while refreshing:
state: {
users: async.stale<User[]>([]), // Shows [] initially, then previous data
}2. Show Offline Indicators
Let users know when they're offline:
{!online && <OfflineBanner />}3. Implement Proper Ping
Don't rely solely on navigator.onLine:
app.set(pingService, () => ({
ping: () => fetch("/api/health").then((r) => r.ok).catch(() => false),
}));4. Add Jitter to Retries
Prevent thundering herd after reconnection:
retry({
retries: 3,
delay: retryStrategy.withJitter(retryStrategy.backoff),
});5. Queue Offline Operations
Store operations to sync later:
if (!network.online) {
state.pendingOperations.push(operation);
return;
}Next Steps
- Network Layer Guide — Building a complete network service
- Abortable Functions — All available wrappers
- Async State — Loading and error states