# React Query Integration for POWR **Last Updated:** April 3, 2025 **Status:** Implementation Plan **Authors:** POWR Development Team ## Table of Contents 1. [Overview](#overview) 2. [Current Architecture Analysis](#current-architecture-analysis) 3. [React Query Integration Strategy](#react-query-integration-strategy) 4. [Authentication & State Management](#authentication--state-management) 5. [Data Fetching & Caching](#data-fetching--caching) 6. [Offline Support & Publication Queue](#offline-support--publication-queue) 7. [Error Handling](#error-handling) 8. [Conflict Resolution](#conflict-resolution) 9. [Implementation Roadmap](#implementation-roadmap) 10. [API Reference](#api-reference) 11. [Migration Guide](#migration-guide) ## Overview This document outlines the plan to integrate React Query (TanStack Query) into the POWR app to resolve critical issues with authentication state transitions, hook ordering, and offline synchronization while preserving our local-first architecture with Nostr integration. ### Key Goals - Fix React hook ordering issues causing crashes during authentication state changes - Provide consistent data access patterns across the application - Enhance offline support with better synchronization - Improve error handling and recovery - Maintain local-first architecture with SQLite and Nostr integration ### Architecture Principles 1. **SQLite Remains Primary Persistence Layer**: All user data will continue to be persisted in SQLite 2. **React Query as Sync/Cache Layer**: React Query manages data synchronization between UI, SQLite, and Nostr 3. **Zustand for UI State**: Active workout state and UI-specific state remain in Zustand stores 4. **NDK for Nostr Integration**: NDK remains the primary interface for Nostr communication ## Current Architecture Analysis ### Existing Data Flow ``` ┌─ UI Components ─────────────────────────────────┐ │ │ │ React Components (screens, etc.) │ │ │ └─────────────────┬───────────────────────────────┘ │ ┌─────────────────▼───────────────────────────────┐ │ │ │ Custom Hooks (useProfile, useSocialFeed, etc.) │ │ │ └───────┬───────────────────────┬─────────────────┘ │ │ ┌───────▼───────────┐ ┌────────▼────────────────┐ │ │ │ │ │ SQLite Services │ │ NDK Integration │ │ (Local Storage) │ │ (Nostr Communication) │ │ │ │ │ └───────────────────┘ └─────────────────────────┘ ``` ### Current Authentication System The current authentication system is implemented across several components: - `lib/auth/AuthService.ts`: Core authentication service - `lib/auth/AuthStateManager.ts`: State machine for auth transitions - `lib/auth/SigningQueue.ts`: Queue for NDK event signing - `lib/hooks/useNDK.ts`: Hook for accessing the NDK instance - `lib/hooks/useNDKAuth.ts`: Hook for authentication with NDK - `lib/signers/NDKAmberSigner.ts`: External signer integration for Android The system implements a state machine pattern: ```typescript type AuthState = | { status: 'unauthenticated' } | { status: 'authenticating', method: 'private_key' | 'amber' | 'ephemeral' } | { status: 'authenticated', user: NDKUser, method: 'private_key' | 'amber' | 'ephemeral' } | { status: 'signing', operationCount: number, operations: SigningOperation[] } | { status: 'error', error: Error }; ``` However, hook ordering issues occur when components conditionally use hooks based on authentication state. ### Current Caching System The current caching system includes: - `SocialFeedCache`: SQLite-based caching for social feed events - `ProfileImageCache`: Caching for user profile images - `ContactCacheService`: Caching for user contact lists - `EventCache`: General-purpose cache for Nostr events - `PublicationQueueService`: Queue for pending Nostr publications ### NDK Integration NDK is integrated through: - `lib/stores/ndk.ts`: NDK store implementation - `lib/hooks/useNDK.ts`: Hook for accessing NDK - `lib/initNDK.ts`: NDK initialization - Various hooks for specific NDK operations ## React Query Integration Strategy ### New Architecture with React Query ``` ┌─ UI Components ─────────────────────────────────┐ │ │ │ React Components (screens, etc.) │ │ │ └─────────────────┬───────────────────────────────┘ │ ▼ ┌─ Data Access Layer ───────────────────────────┐ │ │ │ React Query Hooks │ │ (useProfile, useWorkouts, useTemplates, etc.)│ │ │ └───────┬───────────────────────┬───────────────┘ │ │ ▼ ▼ ┌───────────────────┐ ┌────────────────────────┐ │ SQLite Services │ │ NDK Integration │ │ (local storage) │ │ (network/Nostr) │ └───────────────────┘ └────────────────────────┘ ``` ### Replacement Strategy Our strategy is to use React Query to replace portions of our custom hooks that handle data fetching, caching, and synchronization, while preserving our SQLite services and NDK integration. #### What React Query Will Replace - Custom caching logic in hooks - Manual state management for loading/error states - Ad-hoc fetch retry and synchronization logic - Authentication state propagation #### What Will Remain Unchanged - SQLite database schema and services - NDK event handling and Nostr communication - Zustand stores for active workout state and UI-specific state - Core business logic ### Key Benefits 1. **Consistent Hook Ordering**: React Query preserves hook call order across renders 2. **Automatic Background Refetching**: Built-in mechanisms for data freshness 3. **Standardized Loading/Error States**: Consistent data loading patterns 4. **Optimistic Updates**: Built-in support for optimistic UI updates 5. **Deduplication of Requests**: Prevents redundant network/database calls ## Authentication & State Management ### React Query Auth Provider We'll implement a centralized authentication system using React Query: ```tsx // lib/auth/ReactQueryAuthProvider.tsx import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { createContext, useContext, useState, ReactNode } from 'react'; import { AuthService } from './AuthService'; // Create a new Query Client const queryClient = new QueryClient({ defaultOptions: { queries: { staleTime: 5 * 60 * 1000, // 5 minutes cacheTime: 30 * 60 * 1000, // 30 minutes retry: 3, refetchOnWindowFocus: true, refetchOnMount: true, }, }, }); // Create a context for global access to auth methods const AuthContext = createContext<{ authService: AuthService | null; }>({ authService: null }); // Provider component export function ReactQueryAuthProvider({ children }: { children: ReactNode }) { const [authService] = useState(() => new AuthService()); return ( {children} ); } // Hook for accessing auth service export function useAuthService() { const context = useContext(AuthContext); if (!context) { throw new Error('useAuthService must be used within a ReactQueryAuthProvider'); } return context.authService; } ``` ### Auth Query Hook ```typescript // lib/hooks/useAuthQuery.ts import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useAuthService } from '../auth/ReactQueryAuthProvider'; import * as SecureStore from 'expo-secure-store'; // Auth state query keys export const AUTH_KEYS = { all: ['auth'] as const, current: () => [...AUTH_KEYS.all, 'current'] as const, profile: (pubkey: string) => [...AUTH_KEYS.all, 'profile', pubkey] as const, }; // Hook for managing auth state export function useAuthQuery() { const authService = useAuthService(); const queryClient = useQueryClient(); // Query for current auth state const authQuery = useQuery({ queryKey: AUTH_KEYS.current(), queryFn: async () => { // Check for stored credentials const pubkey = await SecureStore.getItemAsync('nostr_pubkey'); // If no credentials, return unauthenticated state if (!pubkey) { return { status: 'unauthenticated' }; } // Initialize auth from stored credentials try { // For direct key login return await authService.getCurrentAuthState(); } catch (error) { // Handle initialization error console.error('Auth initialization error:', error); // Clear any invalid credentials await SecureStore.deleteItemAsync('nostr_pubkey'); return { status: 'error', error: error instanceof Error ? error : new Error(String(error)) }; } }, // Auth doesn't automatically become stale staleTime: Infinity, }); // Login mutation const loginMutation = useMutation({ mutationFn: async (params: { method: 'private_key' | 'amber' | 'ephemeral'; privateKey?: string; }) => { // Set auth state to authenticating first queryClient.setQueryData(AUTH_KEYS.current(), { status: 'authenticating', method: params.method }); // Perform login based on method try { switch (params.method) { case 'private_key': if (!params.privateKey) throw new Error('Private key required'); return await authService.loginWithPrivateKey(params.privateKey); case 'amber': return await authService.loginWithAmber(); case 'ephemeral': return await authService.createEphemeralKey(); default: throw new Error(`Unsupported auth method: ${params.method}`); } } catch (error) { throw error; } }, onSuccess: (user) => { // Update auth state on success queryClient.setQueryData(AUTH_KEYS.current(), { status: 'authenticated', user, method: user.authMethod }); // Invalidate auth-dependent queries queryClient.invalidateQueries({ queryKey: ['profile'] }); queryClient.invalidateQueries({ queryKey: ['contacts'] }); queryClient.invalidateQueries({ queryKey: ['relays'] }); }, onError: (error) => { // Set error state queryClient.setQueryData(AUTH_KEYS.current(), { status: 'error', error }); } }); // Logout mutation const logoutMutation = useMutation({ mutationFn: async () => { // Perform logout await authService.logout(); }, onSuccess: () => { // Reset auth state queryClient.setQueryData(AUTH_KEYS.current(), { status: 'unauthenticated' }); // Clear user-dependent queries queryClient.removeQueries({ queryKey: ['contacts'] }); queryClient.removeQueries({ queryKey: ['feed'] }); queryClient.removeQueries({ queryKey: ['profile'] }); } }); return { auth: authQuery.data, isLoading: authQuery.isLoading, isError: authQuery.isError, error: authQuery.error, login: loginMutation.mutate, logout: logoutMutation.mutate, isAuthenticating: loginMutation.isPending, isLoggingOut: logoutMutation.isPending, }; } ``` ### Amber Signer Integration ```typescript // lib/hooks/useAmberSignerWithQuery.ts import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useAuthQuery, AUTH_KEYS } from './useAuthQuery'; import { NDKEvent } from '@nostr-dev-kit/ndk'; // Hook for Amber signing operations export function useAmberSigner() { const { auth } = useAuthQuery(); const queryClient = useQueryClient(); // Mutation for signing events const signEventMutation = useMutation({ mutationFn: async (event: NDKEvent) => { if (auth?.status !== 'authenticated') { throw new Error('Not authenticated'); } // Update auth state to show signing in progress queryClient.setQueryData(AUTH_KEYS.current(), { ...auth, status: 'signing', operationCount: (auth.operationCount || 0) + 1, operations: [...(auth.operations || []), { type: 'sign', event, timestamp: Date.now() }] }); try { // Use AmberSignerModule to sign the event const signedEvent = await AmberSignerModule.signEvent( JSON.stringify(event.rawEvent()), event.pubkey ); // Restore auth state after signing queryClient.setQueryData(AUTH_KEYS.current(), auth); return signedEvent; } catch (error) { // Update auth state with error queryClient.setQueryData(AUTH_KEYS.current(), { ...auth, status: 'authenticated', error }); throw error; } } }); return { signEvent: signEventMutation.mutate, isSigningEvent: signEventMutation.isPending, signingError: signEventMutation.error }; } ``` ### Authentication Component Updates Components will need to be updated to use the new auth hooks consistently: ```tsx // Example Login Screen function LoginScreen() { const { login, isAuthenticating, error } = useAuthQuery(); const [privateKey, setPrivateKey] = useState(''); // Handle login const handleLogin = () => { login({ method: 'private_key', privateKey }); }; // Handle Amber login const handleAmberLogin = () => { login({ method: 'amber' }); }; return ( {isAuthenticating ? ( ) : ( <>