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/netinfotypescript
// 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
- Show Offline Status: Always inform users when they're offline
- Cache Data: Store responses locally to show when offline
- Queue Mutations: Queue write operations when offline
- Optimistic Updates: Update UI immediately for better UX
- Handle Errors: Gracefully handle sync errors and revert optimistic updates
- 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
- Performance - Performance optimization
- Custom Storage - Persistent storage solutions
- API Reference - Full API documentation