Vibe coding with AI course Β· Module 8: Slack Clone

Data Export - CSV and PDF Reports

4 min read
In this lesson2

The dashboard looks great, but the quartermaster wants the numbers in a spreadsheet, and the ship owner wants a PDF report. Instead of retyping the table by hand, we'll add two buttons: Export CSV and Export PDF. Both files will be created in the browser, without involving the server.

Exporting to CSV

CSV is plain text: the first row contains the headers, and every following row one record with values separated by commas. The exportToCSV function builds such text and tells the browser to download it, and DataTable calls it with a button:

function exportToCSV(data: any[], filename: string) {
  const headers = Object.keys(data[0]).join(',');
  const rows = data.map(row => Object.values(row).join(','));
  const csv = [headers, ...rows].join('\n');

  const blob = new Blob([csv], { type: 'text/csv' });
  const url = URL.createObjectURL(blob);
  const link = document.createElement('a');
  link.href = url;
  link.download = filename;
  link.click();
}

function DataTable({ data }: { data: any[] }) {
  return (
    <div>
      <button
        onClick={() => exportToCSV(data, 'report.csv')}
        className="bg-green-600 text-white px-4 py-2 rounded mb-4"
      >
        Export CSV
      </button>
      <table className="w-full border">
        {/* Table content */}
      </table>
    </div>
  );
}

There are four steps. Object.keys(data[0]).join(',') creates the header row from the keys of the first object, Object.values(row).join(',') does the same with the values, and we join everything with a newline character (join('\n')). Then a Blob with the type text/csv is created, that is a file kept in the browser's memory. URL.createObjectURL(blob) gives it a temporary address, and link.download = filename and link.click() start the download under the name report.csv. The data in the table stays unchanged. The same pattern downloads any file built in the browser, such as JSON.

For production, three fixes come in handy. A value with a comma, a quote or a newline will break the columns, so wrap it in quotes and double any quote inside, as the CSV standard requires:

const escapeCell = (value: unknown) => `"${String(value).replace(/"/g, '""')}"`;

The function returns new cell text and leaves the original value alone. After the download, release the address with URL.revokeObjectURL(url), and at the very start check data.length, because an empty array will crash the function on data[0].

Exporting to PDF

The jsPDF library will generate the PDF, and its jspdf-autotable add-on will draw the table. You install both together, and the command itself doesn't change any component yet:

npm install jspdf jspdf-autotable

After installation, the export fits in a few lines: you create a document, write a title, draw a table and save the file.

import jsPDF from 'jspdf';
import autoTable from 'jspdf-autotable';

function exportToPDF(data: any[], filename: string) {
  const doc = new jsPDF();

  doc.text('Analytics Report', 14, 15);

  autoTable(doc, {
    head: [Object.keys(data[0])],
    body: data.map(row => Object.values(row))
  });

  doc.save(filename);
}

new jsPDF() creates a document, A4 with dimensions in millimeters by default. doc.text('Analytics Report', 14, 15) writes the title 14 mm from the left and 15 mm from the top, and autoTable draws a table with headers (head) and rows (body). head is an array of header rows, hence the double brackets around Object.keys(data[0]), and body contains one array of values per record. doc.save(filename) downloads the finished file, while the table on the page stays unchanged. The standard PDF fonts only cover a basic character set, so for letters such as Δ… or Ε‚ add your own TTF font. Call the export in a client component, e.g. in onClick, because the download happens in the browser.

Captain Redbeard's tip: a spreadsheet may treat a value starting with =, +, - or @ as a formula. When you export data entered by users, prefix such values with an apostrophe, so nobody can smuggle a formula into your cargo.

Remember: CSV is text in a Blob, PDF is a document from jsPDF, and both reach the user as a download in the browser. In the editor below you have a channel report with a CSV export, and the tasks will check the export steps.

Code for this lesson: App.jsx
// Export Data - CSV download
import { useState } from 'react';

const reportData = [
  { channel: 'treasure-hunts', messages: 142, members: 8 },
  { channel: 'general', messages: 89, members: 12 },
  { channel: 'navigation', messages: 56, members: 5 }
];

function exportToCSV(data, filename) {
  const headers = Object.keys(data[0]).join(',');
  const rows = data.map(row => Object.values(row).join(','));
  const csv = [headers, ...rows].join('\n');

  const blob = new Blob([csv], { type: 'text/csv' });
  const url = URL.createObjectURL(blob);
  const link = document.createElement('a');
  link.href = url;
  link.download = filename;
  link.click();
}

export default function App() {
  return (
    <div className="min-h-screen bg-gray-100 p-8">
      <div className="max-w-3xl mx-auto bg-white border rounded p-6">
        <div className="flex items-center justify-between mb-4">
          <h1 className="text-xl font-bold">Channel Report</h1>
          <button
            onClick={() => exportToCSV(reportData, 'report.csv')}
            className="bg-green-600 text-white px-4 py-2 rounded hover:bg-green-700 text-sm font-semibold"
          >
            Export CSV
          </button>
        </div>

        <table className="w-full border text-left text-sm">
          <thead className="bg-gray-50">
            <tr>
              {Object.keys(reportData[0]).map(key => (
                <th key={key} className="border px-3 py-2 font-semibold">{key}</th>
              ))}
            </tr>
          </thead>
          <tbody>
            {reportData.map((row, idx) => (
              <tr key={idx}>
                {Object.values(row).map((val, i) => (
                  <td key={i} className="border px-3 py-2">{val}</td>
                ))}
              </tr>
            ))}
          </tbody>
        </table>
      </div>
    </div>
  );
}

Spotted a mistake in this lesson?

Check yourself

Answer the questions from this lesson. Pick an answer to see right away whether it is correct.

  1. 1. What MIME type do we set in the Blob when exporting CSV?

  2. 2. How do we create the CSV header row from a data object?

These are 2 of 3 questions for this lesson. Solve the rest in the game.

Hands-on tasks in the game

  • Code editor

    Export data to a CSV file

  • Vertical ordering

    Order the steps for downloading a CSV file in the browser:

Useful articles