Skip to content

Offline Support

Learn how to implement offline functionality with @pubflow/flowfull-client.

WARNING

@pubflow/flowfull-client does NOT include built-in offline support. This guide shows you how to implement offline patterns using external libraries and strategies.

Overview

While @pubflow/flowfull-client doesn't have built-in offline support, you can implement offline functionality using:

  • Network detection libraries
  • Local caching strategies
  • Mutation queuing
  • Optimistic updates

Network Detection

React Native/Expo

Use @react-native-community/netinfo to detect network status:

bash
npx expo install @react-native-community/netinfo
typescript
// src/hooks/useNetworkStatus.ts
import { useState, useEffect } from 'react';
import NetInfo from '@react-native-community/netinfo';

export function useNetworkStatus() {
  const [isOnline, setIsOnline] = useState(true);

  useEffect(() => {
    const unsubscribe = NetInfo.addEventListener(state => {
      setIsOnline(state.isConnected ?? true);
    });

    return () => unsubscribe();
  }, []);

  return { isOnline };
}

Browser

Use the Navigator API:

typescript
// src/hooks/useNetworkStatus.ts
import { useState, useEffect } from 'react';

export function useNetworkStatus() {
  const [isOnline, setIsOnline] = useState(navigator.onLine);

  useEffect(() => {
    function handleOnline() {
      setIsOnline(true);
    }

    function handleOffline() {
      setIsOnline(false);
    }

    window.addEventListener('online', handleOnline);
    window.addEventListener('offline', handleOffline);

    return () => {
      window.removeEventListener('online', handleOnline);
      window.removeEventListener('offline', handleOffline);
    };
  }, []);

  return { isOnline };
}

Offline Indicator

Create a simple offline indicator component:

typescript
// src/components/OfflineIndicator.tsx
import { useNetworkStatus } from '../hooks/useNetworkStatus';

export function OfflineIndicator() {
  const { isOnline } = useNetworkStatus();

  if (isOnline) return null;

  return (
    <div style={{
      position: 'fixed',
      top: 0,
      left: 0,
      right: 0,
      backgroundColor: '#ff6b6b',
      color: 'white',
      padding: '8px',
      textAlign: 'center',
      zIndex: 9999
    }}>
      You are offline. Some features may not be available.
    </div>
  );
}

Mutation Queuing

Implement a simple mutation queue:

typescript
// src/utils/mutationQueue.ts
import { api } from '../api/client';

interface QueuedMutation {
  id: string;
  endpoint: string;
  method: 'POST' | 'PUT' | 'PATCH' | 'DELETE';
  data: any;
  timestamp: number;
}

class MutationQueue {
  private queue: QueuedMutation[] = [];
  private isProcessing = false;

  add(endpoint: string, method: 'POST' | 'PUT' | 'PATCH' | 'DELETE', data: any) {
    const mutation: QueuedMutation = {
      id: `${Date.now()}-${Math.random()}`,
      endpoint,
      method,
      data,
      timestamp: Date.now()
    };

    this.queue.push(mutation);
    this.saveToStorage();
  }

  async process() {
    if (this.isProcessing || this.queue.length === 0) return;

    this.isProcessing = true;

    while (this.queue.length > 0) {
      const mutation = this.queue[0];

      try {
        let response;

        switch (mutation.method) {
          case 'POST':
            response = await api.post(mutation.endpoint, mutation.data);
            break;
          case 'PUT':
            response = await api.put(mutation.endpoint, mutation.data);
            break;
          case 'PATCH':
            response = await api.patch(mutation.endpoint, mutation.data);
            break;
          case 'DELETE':
            response = await api.delete(mutation.endpoint);
            break;
        }

        if (response.success) {
          this.queue.shift(); // Remove from queue
          this.saveToStorage();
        } else {
          break; // Stop processing on error
        }
      } catch (error) {
        break; // Stop processing on error
      }
    }

    this.isProcessing = false;
  }

  private saveToStorage() {
    localStorage.setItem('mutation_queue', JSON.stringify(this.queue));
  }

  private loadFromStorage() {
    const stored = localStorage.getItem('mutation_queue');
    if (stored) {
      this.queue = JSON.parse(stored);
    }
  }

  getQueueLength() {
    return this.queue.length;
  }
}

export const mutationQueue = new MutationQueue();

Using the Queue

typescript
// src/components/CreatePost.tsx
import { useState } from 'react';
import { api } from '../api/client';
import { mutationQueue } from '../utils/mutationQueue';
import { useNetworkStatus } from '../hooks/useNetworkStatus';

function CreatePost() {
  const { isOnline } = useNetworkStatus();
  const [title, setTitle] = useState('');

  async function handleSubmit() {
    if (isOnline) {
      // Online: send immediately
      const response = await api.post('/posts', { title });
      if (response.success) {
        alert('Post created!');
      }
    } else {
      // Offline: queue for later
      mutationQueue.add('/posts', 'POST', { title });
      alert('Post queued. Will be created when online.');
    }
  }

  return (
    <div>
      <input
        value={title}
        onChange={(e) => setTitle(e.target.value)}
        placeholder="Post title"
      />
      <button onClick={handleSubmit}>Create Post</button>
      {!isOnline && <p style={{ color: 'orange' }}>You are offline</p>}
    </div>
  );
}

Process Queue When Online

typescript
// src/App.tsx
import { useEffect } from 'react';
import { useNetworkStatus } from './hooks/useNetworkStatus';
import { mutationQueue } from './utils/mutationQueue';

function App() {
  const { isOnline } = useNetworkStatus();

  useEffect(() => {
    if (isOnline) {
      mutationQueue.process();
    }
  }, [isOnline]);

  return (
    <div>
      <OfflineIndicator />
      <YourApp />
    </div>
  );
}

Data Caching

Cache API responses locally:

typescript
// src/hooks/useCachedQuery.ts
import { useState, useEffect } from 'react';
import { api } from '../api/client';
import { useNetworkStatus } from './useNetworkStatus';

export function useCachedQuery<T>(endpoint: string, cacheKey: string) {
  const [data, setData] = useState<T | null>(null);
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);
  const { isOnline } = useNetworkStatus();

  useEffect(() => {
    fetchData();
  }, [endpoint, isOnline]);

  async function fetchData() {
    // Try to load from cache first
    const cached = localStorage.getItem(cacheKey);
    if (cached) {
      setData(JSON.parse(cached));
      setIsLoading(false);
    }

    // If online, fetch fresh data
    if (isOnline) {
      try {
        const response = await api.get(endpoint);
        if (response.success) {
          setData(response.data);
          localStorage.setItem(cacheKey, JSON.stringify(response.data));
        }
      } catch (err) {
        setError(err as Error);
      } finally {
        setIsLoading(false);
      }
    }
  }

  return { data, isLoading, error, refetch: fetchData };
}

// Usage
function PostList() {
  const { data: posts, isLoading } = useCachedQuery<Post[]>('/posts', 'posts_cache');

  if (isLoading && !posts) return <div>Loading...</div>;

  return (
    <ul>
      {posts?.map(post => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  );
}

Optimistic Updates

Update UI immediately while mutation is pending:

typescript
// src/components/LikeButton.tsx
import { useState } from 'react';
import { api } from '../api/client';

function LikeButton({ postId, initialLikes }: { postId: string; initialLikes: number }) {
  const [likes, setLikes] = useState(initialLikes);
  const [isLiked, setIsLiked] = useState(false);

  async function handleLike() {
    // Optimistic update
    setIsLiked(true);
    setLikes(likes + 1);

    try {
      const response = await api.post('/likes', { postId });

      if (!response.success) {
        // Revert on error
        setIsLiked(false);
        setLikes(likes);
      }
    } catch (error) {
      // Revert on error
      setIsLiked(false);
      setLikes(likes);
    }
  }

  return (
    <button onClick={handleLike} disabled={isLiked}>
      ❤️ {likes} likes
    </button>
  );
}

Best Practices

  1. Show Offline Status: Always inform users when they're offline
  2. Cache Data: Store responses locally to show when offline
  3. Queue Mutations: Queue write operations when offline
  4. Optimistic Updates: Update UI immediately for better UX
  5. Handle Errors: Gracefully handle sync errors and revert optimistic updates
  6. Persist Queue: Use localStorage/AsyncStorage to persist queue across app restarts

Using External Libraries

For more advanced offline support, consider using:

  • TanStack Query - Powerful data fetching with built-in caching
  • SWR - React hooks for data fetching with cache
  • Workbox - Service worker library for PWAs
  • PouchDB - Client-side database with sync capabilities

Next Steps