Skip to main content

Overview

The useCVDraft hook manages CV draft state with automatic saving to localStorage, cloud synchronization, and three-way merge conflict detection. It handles both authenticated and guest user workflows.

Import

import { useCVDraft } from '@/hooks/useCVDraft';

Usage

const CVBuilder = () => {
  const { user, isAuthLoading } = useAuth();
  const form = useFormContext<CVData>();
  
  const {
    isLoading,
    hasPendingSave,
    saveToDraft,
    saveToCloud,
    checkForConflicts,
    syncAfterAuth
  } = useCVDraft({
    user,
    isAuthLoading,
    initialTemplate: 'default',
    onError: (message, context) => {
      console.error(`Error during ${context}:`, message);
      toast.error(message);
    }
  });

  const handleSave = async () => {
    if (user) {
      const success = await saveToCloud();
      if (success) {
        toast.success('CV saved to cloud');
      }
    } else {
      saveToDraft();
      toast.success('Draft saved locally');
    }
  };

  return (
    <div>
      {isLoading ? <Spinner /> : <CVForm />}
      {hasPendingSave && <ConflictBanner />}
    </div>
  );
};

Parameters

user
User | null
required
Firebase authenticated user object. Pass null for guest users.
isAuthLoading
boolean
required
Indicates whether authentication state is still loading. Prevents premature data loading.
initialTemplate
TemplateId
Template to apply when loading CV data. Overrides stored template preference.Options: 'default' | 'rhyhorn' | 'nexus'
onError
(message: string, context: 'load' | 'save' | 'sync') => void
Error callback with user-friendly messages and context.Contexts:
  • 'load' - Failed to load CV data
  • 'save' - Failed to save to cloud
  • 'sync' - Failed to sync after authentication

Return Value

isLoading
boolean
true while initial data is loading. Use to show loading states.
hasPendingSave
boolean
true when local and server data conflict. Indicates user needs to resolve conflicts.
saveToDraft
() => void
Manually save current form data to localStorage draft.Behavior:
  • Only saves if data has changed
  • Updates internal tracking reference
  • Safe to call frequently
saveToCloud
() => Promise<boolean>
Save current form data to Firebase cloud storage.Returns: true on success, false on failureSide effects:
  • Clears localStorage draft on success
  • Updates base version for merge tracking
  • Sets hasPendingSave to false
checkForConflicts
() => Promise<ConflictCheckResult>
Check for conflicts between local draft and server data.Returns:
{
  hasConflict: boolean;
  localData?: CVData;
  serverData?: CVData;
  baseData?: CVData;      // For three-way merge
  localDate?: Date;
  serverDate?: Date;
}
Use cases:
  • Before syncing after login
  • When hasPendingSave is true
  • Manual conflict detection
syncAfterAuth
(newUser: User) => Promise<void>
Sync data immediately after user authentication (login/signup).Parameters:
  • newUser - Newly authenticated Firebase user
Behavior:
  1. Compares local draft with server data
  2. If conflict detected, sets hasPendingSave to true
  3. If no conflict, loads server data or uploads local data
  4. Updates form with resolved data

Features

Auto-Save

Auto-saves draft every 5 seconds when content changes:
const AUTO_SAVE_INTERVAL = 5000; // 5 seconds
Auto-save triggers:
  • Meaningful content exists (personal info, experiences, etc.)
  • Template or section order changes
  • Data differs from last saved state

Data Validation

Automatically sanitizes and validates loaded data:
const sanitizeLoadedData = (rawData: CVData): CVData => {
  return {
    personalInfo: { ...rawData.personalInfo },
    experience: [...rawData.experience],
    sectionOrder: normalizeSectionOrder(rawData.sectionOrder),
    hiddenSections: [...new Set(rawData.hiddenSections)].filter(
      id => id !== 'personal' // Prevent hiding personal section
    ),
    template: isValidTemplateId(rawData.template) ? rawData.template : 'default',
    // ... other fields
  };
};

Conflict Detection

Uses three-way merge strategy:
  1. Base - Last successfully synced version
  2. Local - Current local draft
  3. Server - Current server state
Skips conflict detection if:
  • Local draft has no meaningful content
  • Local and server data are identical
  • User is not authenticated

Error Handling

Provides user-friendly error messages:
const errorMessages = {
  permission: 'Session expired. Please sign in again.',
  network: 'Network error. Please check your connection.',
  quota: 'Storage limit exceeded. Try removing some content.',
  default: 'Failed to load your saved CV data.'
};

Dependencies

Requires React Hook Form context:
import { FormProvider, useForm } from 'react-hook-form';
import { CVData, initialCVData } from '@/lib/types';

const CVBuilderWrapper = () => {
  const form = useForm<CVData>({
    defaultValues: initialCVData
  });

  return (
    <FormProvider {...form}>
      <CVBuilder />
    </FormProvider>
  );
};

State Management

Internal Refs

const lastSavedDataRef = useRef<CVData | null>(null);     // Tracks last saved state
const hasLoadedRef = useRef(false);                        // Prevents duplicate loads
const loadRequestIdRef = useRef(0);                        // Cancels stale requests
const autoSaveIntervalRef = useRef<NodeJS.Timeout | null>(null); // Auto-save timer

Load Priority

  1. Authenticated users: Server data → Local draft (if conflict)
  2. Guest users: Local draft only
  3. After login: Sync local draft with server

Best Practices

Handle Loading States

if (isLoading) {
  return <LoadingSpinner />;
}

return <CVEditor />;

Resolve Conflicts

if (hasPendingSave) {
  const conflict = await checkForConflicts();
  
  if (conflict.hasConflict) {
    // Show conflict resolution UI
    return (
      <ConflictResolver
        localData={conflict.localData}
        serverData={conflict.serverData}
        baseData={conflict.baseData}
        onResolve={(merged) => {
          form.reset(merged);
          await saveToCloud();
        }}
      />
    );
  }
}

Save Before Navigation

useEffect(() => {
  const handleBeforeUnload = (e: BeforeUnloadEvent) => {
    if (form.formState.isDirty) {
      e.preventDefault();
      saveToDraft();
      e.returnValue = '';
    }
  };
  
  window.addEventListener('beforeunload', handleBeforeUnload);
  return () => window.removeEventListener('beforeunload', handleBeforeUnload);
}, [form.formState.isDirty, saveToDraft]);

Build docs developers (and LLMs) love