Skip to main content

Implementing Offline-First Architecture

1. Understanding Offline-First Principles

Principle Description Implementation
Data Synchronization Ensuring data consistency between offline and online states Use libraries like Redux Offline or WatermelonDB
Caching Strategy Storing data locally to reduce dependency on network Implement AsyncStorage or SQLite for local storage
Conflict Resolution Handling data conflicts when re-syncing offline changes Use timestamps or version numbers to resolve conflicts
User Experience Providing feedback to users when offline Display offline indicators and retry options

Example: Detecting Network Status

import { NetInfo } from 'react-native';

const handleNetworkChange = (isConnected) => {
  if (!isConnected) {
    console.log('Offline: Data will be cached locally');
  } else {
    console.log('Online: Syncing data with server');
  }
};

NetInfo.addEventListener('connectionChange', handleNetworkChange);
Note: Offline-first architecture ensures that users can continue to interact with the app even when there is no internet connection.

2. Implementing Local Data Caching

Example: Using AsyncStorage for Local Caching

import AsyncStorage from '@react-native-async-storage/async-storage';

const cacheKey = 'userProfile';
const userData = {
  id: 123,
  name: 'John Doe',
  email: 'john@example.com'
};

// Save to cache
const saveToCache = async () => {
  try {
    await AsyncStorage.setItem(cacheKey, JSON.stringify(userData));
    console.log('Data cached successfully');
  } catch (error) {
    console.error('Failed to cache data', error);
  }
};

// Retrieve from cache
const getFromCache = async () => {
  try {
    const cachedData = await AsyncStorage.getItem(cacheKey);
    if (cachedData) {
      return JSON.parse(cachedData);
    }
    return null;
  } catch (error) {
    console.error('Failed to retrieve cached data', error);
    return null;
  }
};
Method Description Notes
AsyncStorage.setItem(key, value) Sets a key-value pair in storage Value must be stringified JSON
AsyncStorage.getItem(key) Retrieves a value by key Returns string or null
AsyncStorage.removeItem(key) Removes a key-value pair Does not throw if key doesn't exist
AsyncStorage.clear() Removes all keys Use carefully in production
Note: AsyncStorage is not suitable for large data sets or sensitive information. For larger data, consider using SQLite or Realm.

Example: Caching with Expiry

const CACHE_EXPIRY = 5 * 60 * 1000; // 5 minutes

const setCachedWithExpiry = async (key, data) => {
  const item = {
    value: data,
    timestamp: Date.now()
  };
  await AsyncStorage.setItem(key, JSON.stringify(item));
};

const getCachedWithExpiry = async (key) => {
  const cachedItem = await AsyncStorage.getItem(key);
  if (!cachedItem) return null;

  const { value, timestamp } = JSON.parse(cachedItem);
  if (Date.now() - timestamp > CACHE_EXPIRY) {
    await AsyncStorage.removeItem(key);
    return null;
  }
  return value;
};

Best Practices for Local Caching

  • Use AsyncStorage for simple key-value caching
  • Implement cache expiration to prevent stale data
  • Avoid storing sensitive user data in AsyncStorage
  • Consider using libraries like react-native-mmkv for better performance
  • Implement cache invalidation when data changes on the server

3. Using NetInfo for Connectivity Detection

Example: Check Network Status

import { NetInfo } from 'react-native';

const handleConnectivityChange = (isConnected) => {
  console.log('Network status:', isConnected ? 'online' : 'offline');
};

NetInfo.addEventListener('connectionChange', handleConnectivityChange);

// Check current status
NetInfo.fetch().then((state) => {
  console.log('Current network type:', state.type);
  console.log('Is connected:', state.isConnected);
});
Property Description Values
isConnected Indicates if the device is connected to a network true / false
type Network type (e.g., WiFi, cellular) "none", "wifi", "cellular", "unknown"
isConnectionExpensive Indicates if the connection is expensive (e.g., cellular) true / false
isInternetReachable Checks if the device can reach the internet true / false
Note: On iOS, isInternetReachable may return false even when the device is connected to a network if it cannot reach the internet.
Warning: NetInfo is deprecated in favor of react-native-netinfo. Use the new package for better support and features.

Example: Using react-native-netinfo

import NetInfo from '@react-native-community/netinfo';

const handleConnectivityChange = (state) => {
  console.log('Network type:', state.type);
  console.log('Is connected:', state.isConnected);
};

NetInfo.addEventListener(handleConnectivityChange);

// Fetch current network info
NetInfo.fetch().then((state) => {
  console.log('Current network info:', state);
});

4. Implementing Request Queuing

Example: Basic Request Queue Implementation

class RequestQueue {
  constructor() {
    this.queue = [];
    this.isProcessing = false;
  }

  enqueue(request) {
    this.queue.push(request);
    if (!this.isProcessing) {
      this.processQueue();
    }
  }

  async processQueue() {
    this.isProcessing = true;
    while (this.queue.length > 0) {
      const request = this.queue.shift();
      try {
        await this.executeRequest(request);
      } catch (error) {
        console.error('Failed to process request:', error);
      }
    }
    this.isProcessing = false;
  }

  async executeRequest(request) {
    const { url, method, data } = request;
    const response = await fetch(url, {
      method,
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(data),
    });
    return await response.json();
  }
}

const queue = new RequestQueue();
queue.enqueue({
  url: 'https://api.example.com/users',
  method: 'POST',
  data: { name: 'John Doe' },
});
Property Description Default
queue Array storing pending requests []
isProcessing Flag indicating if queue is being processed false
Note: The queue ensures that requests are processed sequentially and only when network connectivity is restored.
Warning: Ensure that queue size is managed to prevent memory overflow in long-running applications.

5. Handling Sync Conflicts

Note: Sync conflict resolution is critical in offline-first apps to maintain data consistency when users go online and sync changes.
Conflict Type Description Resolution Strategy Example
Last Write Wins (LWW) Most recent write overwrites previous values Automatically apply latest change const latest = getLatestChange();\napplyChange(latest);
Merge Strategies Combine changes from multiple sources Manual or automated merge logic const merged = mergeChanges(local, remote);\nupdateStore(merged);
User Decision Require user to choose which version to keep UI prompt for conflict resolution showConflictDialog(local, remote);\nresolveWithUserChoice();

Example: Conflict Detection and Resolution

import { AsyncStorage } from 'react-native';

// Detect conflict based on timestamps or version numbers
const detectConflict = (local, remote) => {
  return local.version !== remote.version;
};

// Resolve using last-write-wins strategy
const resolveConflict = (local, remote) => {
  const latest = new Date(local.timestamp) > new Date(remote.timestamp) ? local : remote;
  return latest;
};

// Save resolved data back to storage
const saveResolved = async (resolved) => {
  await AsyncStorage.setItem('data', JSON.stringify(resolved));
};

Conflict Resolution Approaches

  • Automatic: Apply latest changes without user input
  • Manual: Prompt user to select desired version
  • Merge: Combine changes intelligently (e.g., for non-overlapping fields)
Warning: Always log conflict events for debugging and analytics to understand how often conflicts occur in your app.

6. Using Background Sync

Background sync allows React Native apps to perform network operations even when the app is not in the foreground. This is particularly useful for offline-first architectures where data needs to be synchronized when connectivity is restored.

Feature Description Support
Background Sync API Enables scheduling tasks to run in the background when network connectivity is restored. NEW Available in React Native 0.72+
Task Scheduling Allows scheduling of tasks to run at specific intervals or on network changes. Platform-specific support

Example: Scheduling Background Sync

import { BackgroundFetch } from 'react-native-background-fetch';

const configureBackgroundFetch = async () => {
  await BackgroundFetch.configure({
    stopOnTerminate: false,
    startOnBoot: true,
    enableHeadless: true,
  }, async (taskId) => {
    console.log('[BackgroundFetch] Task executed:', taskId);
    // Perform sync logic here
    await performSync();
    BackgroundFetch.finish(taskId);
  });
};

const performSync = async () => {
  // Sync logic here
  console.log('Syncing data...');
};

configureBackgroundFetch();
Note: Background sync is not available on all platforms. iOS and Android have different capabilities and configurations required.
Warning: Ensure proper handling of network errors and retries to maintain data consistency.

Key Considerations

  • Background sync is not a replacement for real-time data synchronization.
  • Implement proper error handling and retry mechanisms.
  • Use platform-specific configurations for optimal performance.

7. Implementing Optimistic Updates

Optimistic updates improve perceived performance by immediately updating the UI before the server response is received. This technique is particularly useful in mobile apps where network latency can be significant.

Property Description Usage
optimisticUpdate Flag indicating whether to apply optimistic updates true or false
updateFunction Function to apply local state changes Callback function that modifies the UI state
rollbackFunction Function to revert local changes if server fails Callback function that restores previous state

Example: Optimistic Update in a Todo List

const [todos, setTodos] = useState([]);\n\nconst addTodoOptimistically = (newTodo) => {\n  // Optimistically update UI\n  const optimisticTodos = [...todos, { ...newTodo, id: Date.now() }];\n  setTodos(optimisticTodos);\n\n  // Simulate API call\n  setTimeout(() => {\n    fetch('/api/todos', {\n      method: 'POST',\n      body: JSON.stringify(newTodo)\n    })\n    .then(response => response.json())\n    .then(savedTodo => {\n      // If successful, update with real data\n      setTodos(todos.map(todo => \n        todo.id === newTodo.id ? savedTodo : todo\n      ));\n    })\n    .catch(() => {\n      // If failed, rollback to original state\n      setTodos(todos);\n    });\n  }, 1000);\n};
Note: Ensure that the optimistic update matches the server's expected response format to avoid UI inconsistencies.
Warning: Rollback logic must be implemented carefully to prevent UI flickering or incorrect state.

Best Practices for Optimistic Updates

  • Only apply optimistic updates to actions that are likely to succeed (e.g., local mutations, not network errors)
  • Implement rollback logic for failed server requests
  • Use unique identifiers to track local vs. server state
  • Provide visual feedback (e.g., loading indicators) to users during optimistic updates

8. Creating Offline Fallback UI

Example: Basic Offline UI Component

{'import React from &#x27;react&#x27;;\nimport { View, Text, StyleSheet } from &#x27;react-native&#x27;;\n\nconst OfflineFallback = () => {\n  return (\n    <View style={styles.container}>\n      <Text style={styles.title}>Offline Mode</Text>\n      <Text style={styles.message}>You are currently offline. Some features may be limited.</Text>\n    </View>\n  );\n};\n\nconst styles = StyleSheet.create({\n  container: {\n    flex: 1,\n    justifyContent: &#x27;center&#x27;,\n    alignItems: &#x27;center&#x27;,\n    padding: 20,\n  },\n  title: {\n    fontSize: 20,\n    fontWeight: &#x27;bold&#x27;,\n    marginBottom: 10,\n  },\n  message: {\n    fontSize: 16,\n    textAlign: &#x27;center&#x27;,\n  },\n});\n\nexport default OfflineFallback;'}
Property Description Usage
container Root container for the fallback UI Center content with padding
title Text style for the offline title Bold and larger font size
message Text style for the offline message Centered and readable font size
Note: The fallback UI should be minimal and clearly indicate the offline state to the user.
Warning: Ensure that the fallback UI is accessible and provides clear instructions for re-establishing connectivity.

Key Points

  • Display a clear offline indicator to the user.
  • Provide actionable feedback or instructions for reconnection.
  • Keep the UI simple and unobtrusive to avoid confusion.

9. Managing Data Reconciliation

Example: Basic Offline Data Sync Strategy

import AsyncStorage from '@react-native-async-storage/async-storage';

// Store local changes
const storeLocalChanges = async (changes) => {
  const localChanges = await AsyncStorage.getItem('localChanges');
  const changesArray = localChanges ? JSON.parse(localChanges) : [];
  changesArray.push(changes);
  await AsyncStorage.setItem('localChanges', JSON.stringify(changesArray));
};

// Sync with server
const syncWithServer = async () => {
  const localChanges = await AsyncStorage.getItem('localChanges');
  if (!localChanges) return;

  const changesArray = JSON.parse(localChanges);
  try {
    const response = await fetch('/api/sync', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(changesArray),
    });

    if (response.ok) {
      await AsyncStorage.removeItem('localChanges');
    }
  } catch (error) {
    console.error('Sync failed:', error);
  }
};
Property Description Values Notes
localChanges Stores pending changes Array Used for offline data reconciliation
syncWithServer Syncs local changes to server function Handles conflict resolution logic
Note: Conflicts should be resolved by comparing timestamps or version numbers.
Warning: Ensure all local changes are persisted before attempting sync.

Key Points for Data Reconciliation

  • Local changes are stored in AsyncStorage
  • Server sync is performed when network is available
  • Conflict resolution logic must be implemented
  • Changes are cleared only after successful sync

10. Testing Offline Scenarios

Example: Mocking Network Requests

import { NetInfo } from 'react-native';

// Mock network status
NetInfo.fetch().then((state) => {
  console.log("Network type:", state.type);
  console.log("Is connected:", state.isConnected);
});

// Simulate offline
NetInfo.addEventListener("connectionChange", (netInfo) => {
  if (!netInfo.isConnected) {
    console.log("Offline");
  }
});
Method Description Return Value
NetInfo.fetch() Fetches current network information Promise<NetworkInfo>
NetInfo.addEventListener() Subscribes to network changes unsubscribe function

Testing Offline Scenarios

  • Use NetInfo to detect network status
  • Simulate offline conditions for testing
  • Implement fallback UI when network is unavailable

Was this article helpful?