DEV Community

Ahmad Faraz
Ahmad Faraz

Posted on

Building a Modern CV Builder with React 19, TypeScript, and Firebase: A Complete Guide

Learn how to build a professional, open-source CV/Resume builder application with real-time preview, PDF export, and Firebase integration.

🎯 Introduction

In this comprehensive guide, I'll walk you through building a modern CV/Resume builder application from scratch. This project demonstrates advanced React patterns, TypeScript best practices, Firebase integration, and performance optimization techniques that you can apply to your own projects.

What we're building:

  • A full-featured CV builder with 11 beautiful templates
  • Real-time preview with side-by-side editing
  • PDF export and import functionality
  • Firebase authentication and data persistence
  • Performance-optimized components
  • A 6-step guided wizard for resume creation

Repository: GitHub - resume-studio

🚀 Project Overview

This CV builder is a production-ready application that showcases modern web development practices. It's built with React 19, TypeScript, and Firebase, providing a seamless user experience for creating professional resumes.

Key Features

  • 11 Professional Templates - From minimalist to modern designs
  • 🎨 Real-time Preview - See changes instantly as you type
  • 📄 PDF Export/Import - Export your CV or upload existing PDFs
  • 🔐 Firebase Authentication - Email, Google, and Twitter sign-in
  • 💾 Auto-save - Your progress is automatically saved
  • Performance Optimized - Memoized components prevent unnecessary re-renders
  • 📱 Responsive Design - Works perfectly on all devices

🛠️ Tech Stack

Here's what powers this application:

  • React 19 - Latest React with hooks and memoization
  • TypeScript - Type-safe development
  • Vite 7 - Lightning-fast build tool
  • Tailwind CSS - Utility-first styling
  • Firebase - Authentication, Firestore, and Storage
  • React Router v7 - Client-side routing
  • jsPDF & html2canvas - PDF generation
  • pdfjs-dist - PDF parsing and data extraction

📐 Architecture & Design Patterns

1. Component Structure

The application follows a modular component architecture:

src/
├── components/
│   ├── common/           # Reusable UI components
│   ├── dashboard/        # Dashboard components
│   ├── layout/           # Layout components
│   ├── templates/        # 11 CV template components
│   └── wizard/           # Wizard step components
├── hooks/                # Custom React hooks
├── services/             # Firebase services
├── contexts/             # React contexts
└── utils/                # Utility functions
Enter fullscreen mode Exit fullscreen mode

2. Custom Hooks for State Management

We use custom hooks to encapsulate state logic and make components cleaner:

// src/hooks/useCVData.ts
import { useState, useEffect, useCallback } from 'react';
import type { CVData } from '../types/cv.types';
import { saveCVData, loadCVData } from '../utils/dataPersistence';

export const useCVData = () => {
  const [cvData, setCvData] = useState<CVData>(freshCVData);
  const [isLoading, setIsLoading] = useState(true);

  useEffect(() => {
    const savedData = loadCVData();
    if (savedData) {
      setCvData(savedData);
    }
    setIsLoading(false);
  }, []);

  useEffect(() => {
    if (!isLoading) {
      saveCVData(cvData);
    }
  }, [cvData, isLoading]);

  const updateCVData = useCallback((newData: CVData) => {
    setCvData(newData);
  }, []);

  return { cvData, updateCVData, isLoading };
};
Enter fullscreen mode Exit fullscreen mode

This hook provides:

  • Automatic loading from localStorage
  • Auto-save functionality
  • Stable function references with useCallback

3. Performance Optimization with React.memo

One of the critical challenges in a real-time editor is preventing unnecessary re-renders. Here's how we solved it:

The Problem

When typing in an input field, every keystroke would trigger a state update, causing the entire component tree to re-render. This led to:

  • Input fields losing focus
  • Poor typing performance
  • Unnecessary DOM updates

The Solution

We memoized components and used refs to maintain stable function references:

// src/components/common/Input.tsx
import { memo, useMemo } from 'react';
import type { FC, InputHTMLAttributes } from 'react';

interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
  label?: string;
  error?: string;
  helperText?: string;
}

const Input: FC<InputProps> = ({ 
  label, 
  error, 
  helperText, 
  className = '', 
  id,
  ...props 
}) => {
  const inputId = useMemo(() => 
    id || `input-${Math.random().toString(36).substr(2, 9)}`, 
    [id]
  );

  // ... rest of component
};

export default memo(Input);
Enter fullscreen mode Exit fullscreen mode

CollapsibleSection Optimization

The biggest performance win came from moving CollapsibleSection outside the parent component:

// ❌ Before: Defined inside component (recreated on every render)
const CVEditorCollapsible = ({ data, onChange }) => {
  const CollapsibleSection = ({ ... }) => ( ... ); // Recreated!
  return <CollapsibleSection ... />;
};

// ✅ After: Defined outside (stable component identity)
const CollapsibleSection: FC<CollapsibleSectionProps> = memo(({ 
  title, icon, iconColor, children, isExpanded, onToggle 
}) => (
  <div className="bg-white rounded-2xl shadow-lg overflow-hidden mb-6">
    {/* ... */}
  </div>
));

const CVEditorCollapsible = ({ data, onChange }) => {
  return <CollapsibleSection ... />;
};
Enter fullscreen mode Exit fullscreen mode

Why this matters: When CollapsibleSection was defined inside the parent, React saw it as a new component type on every render, causing all children to unmount and remount. Moving it outside gives it a stable identity, allowing React to properly optimize re-renders.

4. Firebase Integration

Authentication Setup

// src/config/firebase.ts
import { initializeApp } from 'firebase/app';
import { getAuth } from 'firebase/auth';
import { getFirestore } from 'firebase/firestore';
import { getStorage } from 'firebase/storage';

const firebaseConfig = {
  apiKey: import.meta.env.VITE_FIREBASE_API_KEY,
  authDomain: import.meta.env.VITE_FIREBASE_AUTH_DOMAIN,
  projectId: import.meta.env.VITE_FIREBASE_PROJECT_ID,
  storageBucket: import.meta.env.VITE_FIREBASE_STORAGE_BUCKET,
  messagingSenderId: import.meta.env.VITE_FIREBASE_MESSAGING_SENDER_ID,
  appId: import.meta.env.VITE_FIREBASE_APP_ID,
};

const app = initializeApp(firebaseConfig);
export const auth = getAuth(app);
export const db = getFirestore(app);
export const storage = getStorage(app);
Enter fullscreen mode Exit fullscreen mode

Service Layer Pattern

We use a service layer to abstract Firebase operations:

// src/services/authService.ts
import { 
  signInWithEmailAndPassword,
  createUserWithEmailAndPassword,
  signInWithPopup,
  GoogleAuthProvider,
  TwitterAuthProvider,
  onAuthStateChanged,
} from 'firebase/auth';
import { auth } from '../config/firebase';

export const signInUser = async (email: string, password: string) => {
  return await signInWithEmailAndPassword(auth, email, password);
};

export const signUpUser = async (email: string, password: string) => {
  return await createUserWithEmailAndPassword(auth, email, password);
};

export const signInWithGoogle = async () => {
  const provider = new GoogleAuthProvider();
  return await signInWithPopup(auth, provider);
};

export const onAuthStateChange = (callback: (user: any) => void) => {
  return onAuthStateChanged(auth, callback);
};
Enter fullscreen mode Exit fullscreen mode

This pattern provides:

  • Centralized Firebase logic
  • Easy testing and mocking
  • Consistent error handling
  • Type safety

🎨 Key Features Implementation

1. Real-time Preview

The editor uses a two-column layout with synchronized state:

// src/pages/EditorPage.tsx
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12">
  {/* Left: Editor */}
  <div className="overflow-y-auto pr-2">
    <CVEditorCollapsible 
      data={cvData} 
      onChange={(newData) => updateCVData(newData)} 
    />
  </div>

  {/* Right: Preview */}
  <div className="overflow-y-auto">
    <div id="cv-export-content">
      {renderTemplate(selectedTemplate, cvData)}
    </div>
  </div>
</div>
Enter fullscreen mode Exit fullscreen mode

Every change in the editor updates cvData, which triggers a re-render of the preview panel.

2. PDF Export

PDF export uses html2canvas and jsPDF:

// src/utils/exportPDF.ts
import html2canvas from 'html2canvas';
import jsPDF from 'jspdf';

export const exportToPDF = async (
  elementId: string,
  filename: string
): Promise<void> => {
  const element = document.getElementById(elementId);
  if (!element) throw new Error('Element not found');

  const canvas = await html2canvas(element, {
    scale: 2,
    useCORS: true,
    logging: false,
  });

  const imgData = canvas.toDataURL('image/png');
  const pdf = new jsPDF('p', 'mm', 'a4');
  const imgWidth = 210; // A4 width in mm
  const imgHeight = (canvas.height * imgWidth) / canvas.width;

  pdf.addImage(imgData, 'PNG', 0, 0, imgWidth, imgHeight);
  pdf.save(filename);
};
Enter fullscreen mode Exit fullscreen mode

3. PDF Import & Data Extraction

Users can upload existing PDFs and extract data:

// src/utils/exportPDF.ts (simplified)
import * as pdfjsLib from 'pdfjs-dist';

export const extractTextFromPDF = async (file: File): Promise<string> => {
  const arrayBuffer = await file.arrayBuffer();
  const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise;

  let fullText = '';
  for (let i = 1; i <= pdf.numPages; i++) {
    const page = await pdf.getPage(i);
    const textContent = await page.getTextContent();
    const pageText = textContent.items
      .map((item: any) => item.str)
      .join(' ');
    fullText += pageText + '\n';
  }

  return fullText;
};
Enter fullscreen mode Exit fullscreen mode

4. 6-Step Wizard

The wizard guides users through resume creation:

// src/pages/ResumeWizard.tsx
const steps = [
  { id: 'personal', title: 'Personal Information', component: PersonalInfoStep },
  { id: 'work', title: 'Work Experience', component: WorkExperienceStep },
  { id: 'education', title: 'Education', component: EducationStep },
  { id: 'skills', title: 'Skills', component: SkillsStep },
  { id: 'summary', title: 'Summary', component: SummaryStep },
  { id: 'review', title: 'Review', component: ReviewStep },
];

const handleComplete = () => {
  updateCVData(wizardData);
  const selectedTemplate = localStorage.getItem('selectedTemplate') || '1';
  navigate(`/editor/${selectedTemplate}`);
};
Enter fullscreen mode Exit fullscreen mode

Each step validates input before allowing progression, ensuring data quality.

🔧 Performance Optimizations

1. Component Memoization

We memoized all frequently re-rendered components:

  • Input, Textarea, Date - Using React.memo
  • CollapsibleSection - Moved outside and memoized
  • Event handlers - Using useCallback

2. Stable Function References

const handlePersonalInfoChange = useCallback((field: string, value: string) => {
  const currentData = dataRef.current;
  onChangeRef.current({
    ...currentData,
    personalInfo: {
      ...currentData.personalInfo,
      [field]: value,
    },
  });
}, []); // Empty deps - uses refs instead
Enter fullscreen mode Exit fullscreen mode

By using refs (dataRef, onChangeRef), we can keep the dependency array empty, ensuring the function reference never changes.

3. Local Storage Caching

CV data is automatically cached in localStorage:

useEffect(() => {
  if (!isLoading) {
    saveCVData(cvData); // Auto-save on every change
  }
}, [cvData, isLoading]);
Enter fullscreen mode Exit fullscreen mode

This provides:

  • Instant loading on page refresh
  • Offline capability
  • Reduced Firebase read operations

📱 Getting Started

Prerequisites

  • Node.js 16+
  • npm or yarn
  • Firebase account (free tier works)

Installation

# Clone the repository
git clone https://github.com/AhmadFaraz-crypto/resume-studio.git
cd resume-studio

# Install dependencies
npm install

# Set up environment variables
cp env.example .env.local

# Add your Firebase config to .env.local
VITE_FIREBASE_API_KEY=your-api-key
VITE_FIREBASE_AUTH_DOMAIN=your-project.firebaseapp.com
VITE_FIREBASE_PROJECT_ID=your-project-id
VITE_FIREBASE_STORAGE_BUCKET=your-project.appspot.com
VITE_FIREBASE_MESSAGING_SENDER_ID=123456789
VITE_FIREBASE_APP_ID=your-app-id

# Start development server
npm run dev
Enter fullscreen mode Exit fullscreen mode

Firebase Setup

  1. Create Firebase Project

  2. Enable Services

    • Authentication: Enable Email/Password, Google, Twitter
    • Firestore: Create database in test mode
    • Storage: Create storage bucket in test mode
  3. Get Configuration

    • Project Settings → Your apps → Web app
    • Copy config values to .env.local

🎯 Key Learnings

1. Performance Matters

Memoization isn't just about optimization—it's about user experience. Preventing unnecessary re-renders ensures:

  • Smooth typing experience
  • No focus loss
  • Better perceived performance

2. Component Architecture

Moving components outside their parents when they're recreated on every render is crucial. React needs stable component identities for proper optimization.

3. TypeScript is Your Friend

Strong typing catches bugs early and makes refactoring safer:

interface CVData {
  personalInfo: {
    name: string;
    email: string;
    phone: string;
    location: {
      city: string;
      zipCode: string;
      country: string;
    };
    linkedin?: string;
    summary: string;
  };
  workExperience: WorkExperience[];
  education: Education[];
  skills: string[];
}
Enter fullscreen mode Exit fullscreen mode

4. Service Layer Pattern

Abstracting Firebase operations into services provides:

  • Better testability
  • Easier maintenance
  • Consistent error handling

🚀 Deployment

The application can be deployed to any static hosting platform:

Netlify

# Build command
npm run build

# Publish directory
dist
Enter fullscreen mode Exit fullscreen mode

Add environment variables in Netlify dashboard.

Vercel

# Framework: Vite
# Build command: npm run build
# Output directory: dist
Enter fullscreen mode Exit fullscreen mode

Environment variables are configured in Vercel dashboard.

🤝 Contributing

Contributions are welcome! Areas for contribution:

  • 🎨 New template designs
  • 🐛 Bug fixes
  • 📚 Documentation improvements
  • ♿ Accessibility enhancements
  • 🌍 Internationalization
  • 🧪 Tests
  • 🎯 Performance optimizations

📚 Resources

🎉 Conclusion

Building this CV builder taught me valuable lessons about:

  • React performance optimization
  • TypeScript best practices
  • Firebase integration patterns
  • Component architecture
  • User experience design

The application is fully open-source and production-ready. You can use it as:

  • A learning resource
  • A starting point for your own projects
  • A portfolio showcase
  • A contribution opportunity

Star the repository if you find it helpful: GitHub - resume-studio


💬 Questions?

Have questions or suggestions? Feel free to:

  • Open an issue on GitHub
  • Start a discussion
  • Submit a pull request

Top comments (0)