Hone logo
Hone
Problems

React Offline-First State Management

Building modern web applications often requires a seamless user experience, even in the face of intermittent network connectivity. This challenge focuses on implementing an offline-first state management strategy in a React application using TypeScript, ensuring data is always available and changes are synchronized when connectivity is restored.

Problem Description

Your task is to build a simple to-do list application that functions seamlessly offline. The application should allow users to add, mark as complete, and delete to-do items. When the application is offline, these changes should be stored locally and then synchronized with a remote backend (simulated for this challenge) once the network connection is re-established.

Key Requirements:

  1. Offline Data Storage: All to-do items and their states (e.g., completed, deleted) must be persisted locally so they are accessible when offline.
  2. Online/Offline Detection: The application needs to detect when it is online or offline.
  3. Local State Management: React components should always display the current state of the to-do list, whether from local storage or the remote backend.
  4. Change Queueing: Any mutations (add, complete, delete) performed while offline should be queued locally.
  5. Synchronization: When the application comes back online, the queued changes must be sent to the remote backend in the correct order.
  6. Conflict Resolution (Basic): For simplicity, assume that if a to-do item is modified both locally and remotely before synchronization, the remote change takes precedence for now. In a real-world scenario, more sophisticated conflict resolution would be needed.
  7. UI Feedback: Provide clear visual feedback to the user indicating whether the application is online, offline, or synchronizing.

Expected Behavior:

  • When online, the app fetches data from the "remote backend" and displays it.
  • When the network connection drops, the app continues to function using locally stored data. New items can be added, marked complete, or deleted.
  • When the network connection is restored, the locally queued changes are sent to the "remote backend" and applied. The UI should then reflect the synchronized state.
  • If a to-do item is deleted offline and then marked complete online before sync, the deletion should win.

Edge Cases:

  • Initial load when the device is offline.
  • Rapidly switching between online and offline states.
  • User performing multiple actions while offline.

Examples

Example 1: Adding an item while offline

Input:
- App starts online, fetches initial todos: [{ id: '1', text: 'Buy milk', completed: false }]
- User goes offline.
- User adds a new todo: 'Walk the dog'

Output (Local Storage):
- Todos: [{ id: '1', text: 'Buy milk', completed: false }, { id: '2', text: 'Walk the dog', completed: false }]
- Pending Operations Queue: [{ type: 'ADD', payload: { id: '2', text: 'Walk the dog', completed: false } }]

Explanation: The new todo is added to the local state and queued for synchronization. The UI displays both items.

Example 2: Completing an item while offline and syncing

Input:
- User is offline.
- Locally stored todos: [{ id: '1', text: 'Buy milk', completed: false }]
- User marks 'Buy milk' as complete.
- User comes back online.

Output (After Sync):
- Remote Backend receives: [{ type: 'UPDATE', payload: { id: '1', text: 'Buy milk', completed: true } }]
- Remote Backend State: [{ id: '1', text: 'Buy milk', completed: true }]
- Local Storage (after successful sync): [{ id: '1', text: 'Buy milk', completed: true }]
- Pending Operations Queue: []

Explanation: The completion is queued, then sent to the backend once online. The remote backend is updated, and the local state reflects the synchronized version.

Example 3: Deleting an item offline, then updating it online (before sync)

Input:
- App is online, initial todos: [{ id: '1', text: 'Buy milk', completed: false }]
- User goes offline.
- User deletes 'Buy milk'.
- While still offline, user somehow (e.g., through another device, or a delayed effect) changes 'Buy milk' to be completed remotely.
- User comes back online.

Output (After Sync - assuming delete takes precedence):
- Remote Backend receives: [{ type: 'DELETE', payload: { id: '1' } }]
- Remote Backend State: []
- Local Storage (after successful sync): []
- Pending Operations Queue: []

Explanation: The delete operation was queued first. When the app comes online, the queued delete operation is sent to the backend. Even though there was a pending online update to the same item, the delete operation is processed, effectively removing it from the backend and local storage. (Note: This is a simplified conflict. In a real app, you might merge states or use more complex resolution).

Constraints

  • Backend Simulation: You will simulate the backend using a simple JavaScript object or an array. Network requests will be simulated using setTimeout to mimic network latency.
  • Local Storage: Use the browser's localStorage API for persistent offline storage.
  • State Management: You can use React's built-in useState and useReducer, or a library like Zustand or Jotai. The core challenge is the offline-first logic, not the choice of state management library.
  • Network Detection: Use the browser's navigator.onLine property and potentially listen to online and offline events.
  • Performance: The application should remain responsive even with a moderate number of to-do items (e.g., up to 100).

Notes

  • Consider how you will generate unique IDs for new to-do items.
  • Think about the data structure for your local storage and the queue of pending operations.
  • The navigator.onLine property can sometimes be unreliable. For a more robust solution, you might implement periodic "heartbeat" checks to the backend.
  • When synchronizing, ensure operations are sent in the order they were performed to avoid race conditions and logical errors.
  • For this challenge, you don't need to implement complex authentication or authorization. Focus purely on the offline-first data synchronization aspect.
  • You'll need to create mock API functions for fetchTodos, addTodo, updateTodo, and deleteTodo that simulate network latency and success/failure responses.
Loading editor...
typescript