Building mobile apps that work offline is no longer a luxury—it's a necessity. Users often encounter situations with poor or no internet connectivity. Whether they’re underground, traveling, or in remote areas, they expect your app to work regardless of network status. This blog explores how to build offline-ready Expo apps using powerful tools like AsyncStorage, NetInfo, and smart syncing strategies.
🚀 Why Offline Support Matters
Imagine using a to-do list or note-taking app only to lose access because of bad internet. Poor offline support leads to bad user experience and app uninstalls. Offline support ensures:
- Seamless user experience
- Higher retention and engagement
- Functional core features even in airplane mode
- Better perception of reliability and polish
Apps like WhatsApp, Google Docs, and Notion are great examples—they continue to work even without a connection and sync data when the internet is back.
🔧 Tools We’ll Use
To enable offline support, we’ll combine a few key tools:
- AsyncStorage: For local data persistence
- NetInfo: To detect connection status
- State management (e.g. useState / Redux / Zustand)
- Syncing logic: To store changes and push them when online
🛠️ Step-by-Step: Building a Basic Offline Note App
Let’s build a simple note-taking app that allows users to write notes offline, store them locally, and sync them when the app detects internet access.
Step 1: Create a New Expo Project
expo init offline-notes-app
cd offline-notes-app
Choose the blank template (JavaScript or TypeScript).
Step 2: Install Dependencies
npm install @react-native-async-storage/async-storage @react-native-community/netinfo
These libraries help us store and retrieve data locally, and detect network changes.
Step 3: Setup AsyncStorage
Create a utility file to manage local storage.
utils/storage.js
import AsyncStorage from "@react-native-async-storage/async-storage";
export const saveNotes = async (notes) => {
try {
const json = JSON.stringify(notes);
await AsyncStorage.setItem("@notes", json);
} catch (e) {
console.error("Saving error:", e);
}
};
export const loadNotes = async () => {
try {
const json = await AsyncStorage.getItem("@notes");
return json != null ? JSON.parse(json) : [];
} catch (e) {
console.error("Loading error:", e);
return [];
}
};
Step 4: Check Connectivity with NetInfo
Detect network status and sync accordingly.
hooks/useNetwork.js
import { useEffect, useState } from "react";
import NetInfo from "@react-native-community/netinfo";
export const useNetworkStatus = () => {
const [isConnected, setIsConnected] = useState(true);
useEffect(() => {
const unsubscribe = NetInfo.addEventListener((state) => {
setIsConnected(state.isConnected);
});
return () => unsubscribe();
}, []);
return isConnected;
};
Step 5: Implement Offline Notes in App.js
import React, { useEffect, useState } from "react";
import {
View,
TextInput,
Button,
FlatList,
Text,
StyleSheet,
} from "react-native";
import { loadNotes, saveNotes } from "./utils/storage";
import { useNetworkStatus } from "./hooks/useNetwork";
export default function App() {
const [note, setNote] = useState("");
const [notes, setNotes] = useState([]);
const isConnected = useNetworkStatus();
useEffect(() => {
(async () => {
const localNotes = await loadNotes();
setNotes(localNotes);
})();
}, []);
useEffect(() => {
saveNotes(notes);
}, [notes]);
const addNote = () => {
if (!note.trim()) return;
const newNote = { id: Date.now().toString(), content: note };
setNotes((prev) => [newNote, ...prev]);
setNote("");
};
return (
<View style={styles.container}>
<TextInput
style={styles.input}
value={note}
placeholder="Write a note..."
onChangeText={setNote}
/>
<Button title="Save Note" onPress={addNote} />
{!isConnected && <Text style={styles.offline}>Offline Mode</Text>}
<FlatList
data={notes}
keyExtractor={(item) => item.id}
renderItem={({ item }) => (
<Text style={styles.note}>{item.content}</Text>
)}
/>
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1, padding: 20, paddingTop: 60 },
input: { borderWidth: 1, borderColor: "#ccc", padding: 10, marginBottom: 10 },
note: { padding: 10, borderBottomWidth: 1, borderColor: "#eee" },
offline: { color: "red", marginTop: 10 },
});
🔁 Smart Syncing Strategy
Once connectivity is restored, you can:
- Push new or changed notes to your backend
- Sync deletions or edits
- Resolve conflicts (latest timestamp wins or manual conflict resolution)
You might track unsynced changes in a separate array and attempt syncing when isConnected
is true
.
Example Sync Logic (Pseudo-code)
useEffect(() => {
if (isConnected && pendingChanges.length > 0) {
syncToServer(pendingChanges);
clearPendingChanges();
}
}, [isConnected]);
💡 Tips for Better Offline UX
- Show a clear offline indicator
- Queue user actions and process them when back online
- Cache images, API responses, and forms
- Use local-first design: write locally, then sync
🔮 What’s Next?
Take your offline experience even further:
- Database layer: Use Realm or SQLite for structured offline data
- Background sync: With
expo-task-manager
or push notifications - Service workers (PWA) if targeting the web
- Delta sync: Send only what’s changed, not the full payload
✅ Conclusion
Building offline-ready apps with Expo is achievable and incredibly rewarding. By using tools like AsyncStorage and NetInfo, you ensure your app is always usable and never leaves the user stranded. Combine good UX with solid sync strategies and you're on your way to creating a world-class mobile experience.
Offline is not an afterthought. Build it in from the beginning, and your users will thank you.