Skip to main content

Next.js & SheetJS: How to Read, Display & Export Excel Spreadsheets

Integrating spreadsheet operations into a Next.js application requires balancing build-time data pipelines with client-side bundle performance. Using SheetJS (the xlsx library), developers can process workbooks during static builds, fetch them on demand in server components, or parse and export them directly in the browser.


Interactive Pipeline: Next.js & SheetJS Lifecycle

Select any stage to inspect the execution environment, SheetJS API methods, and output formats.

Stage 1: Raw Data Acquisition Ingestion
Spreadsheets enter the app via filesystem buffers at build time, remote fetch queries, or browser file inputs.
const buffer = await fs.readFile(path); // Server / Build


Package Installation

Modern builds of SheetJS are distributed via the official SheetJS CDN to guarantee the latest release:

npm i --save https://cdn.sheetjs.com/xlsx-latest/xlsx-latest.tgz


Do You Need generateStaticParams?

A common question when parsing files in Next.js is whether generateStaticParams is strictly required. The requirement depends directly on your route structure and deployment target:

  • Flat Routes (e.g., app/catalog/page.tsx): generateStaticParams is never needed. The server component reads the file during build or on request without route parameters.
  • Dynamic Routes on a Node Server (e.g., app/catalog/[id]/page.tsx): generateStaticParams is optional. Pages can be rendered on-demand via standard Server-Side Rendering (SSR).
  • Dynamic Routes with Static Export (output: 'export'): generateStaticParams is mandatory. Because no live server exists at runtime, Next.js must pre-render every single dynamic parameter during next build.
  • Unspecified Paths: By default, Next.js uses dynamicParams: true to render ungenerated dynamic segments on request. Setting dynamicParams: false will return a 404 for any slug not explicitly returned by generateStaticParams.


Pattern 1: Server-Side Ingestion (SSG & Dynamic Routes)

Parsing on the server isolates the spreadsheet parser from the browser bundle, keeping client-side JavaScript minimal. The following example parses a spreadsheet to populate a page or dynamic parameters:

import { read, utils } from 'xlsx';

import { promises as fs } from 'fs';

import path from 'path';


// 1. Reusable helper to read and parse local workbooks

export async function getCatalogFromSheet() {

  const filePath = path.join(process.cwd(), 'data', 'catalog.xlsx');

  const fileBuffer = await fs.readFile(filePath);


  // Parse buffer with cellDates enabled

  const workbook = read(fileBuffer, { type: 'buffer', cellDates: true });

  const worksheet = workbook.Sheets[workbook.SheetNames[0]];


  // Transform sheet records into an array of typed objects

  return utils.sheet_to_json(worksheet);

}


// 2. Optional: Generate static routes if using output: 'export'

export async function generateStaticParams() {

  const items = await getCatalogFromSheet();

  return items.map((item) => ({ id: String(item.ID) }));

}


// 3. Server Component Page

export default async function CatalogPage() {

  const items = await getCatalogFromSheet();

  return (

    <main>

      <h1>Product Catalog</h1>

      <ul>

        {items.map((item, idx) => (

          <li key={idx}>{item.Name} - ${item.Price}</li>

        ))}

      </ul>

    </main>

  );

}


Pattern 2: Client-Side Drag-and-Drop Ingestion

When users upload workbooks in the browser, using dynamic imports ensures the SheetJS library is only downloaded when a file is actually selected:

'use client';


import React, { useState } from 'react';


export function SpreadsheetUploader() {

  const [data, setData] = useState([]);


  const handleFileUpload = async (event) => {

    const file = event.target.files?.[0];

    if (!file) return;


    // Lazy load SheetJS to preserve initial page load performance

    const { read, utils } = await import('xlsx');


    const arrayBuffer = await file.arrayBuffer();

    // Wrap in Uint8Array for cross-browser safety

    const workbook = read(new Uint8Array(arrayBuffer), { type: 'array', cellDates: true });


    const firstSheet = workbook.Sheets[workbook.SheetNames[0]];

    const parsedRows = utils.sheet_to_json(firstSheet);

    setData(parsedRows);

  };


  return (

    <div>

      <input type="file" accept=".xlsx, .xls, .csv" onChange={handleFileUpload} />

      <p>Loaded Rows: {data.length}</p>

    </div>

  );

}


Pattern 3: Data Presentation Options

SheetJS offers two primary utility functions for formatting sheet data into the view layer:

  • Arrays of Objects (utils.sheet_to_json): Converts rows into standard JavaScript objects, making it the preferred approach for React state, pagination, and sorting.
  • Static HTML Markup (utils.sheet_to_html): Generates a pure HTML table string, ideal for quick previews without creating React virtual DOM overhead.

// Generating direct HTML markup

const htmlString = utils.sheet_to_html(worksheet);


export function RawSheetPreview({ html }) {

  return <div dangerouslySetInnerHTML={{ __html: html }} />;

}


Pattern 4: Client-Side XLSX Export

Exporting client state or table records back to an Excel file can also be dynamically loaded on demand:

'use client';


export function ExportButton({ records, fileName = 'report.xlsx' }) {

  const handleExport = async () => {

    // Dynamically import export utilities

    const { utils, writeFile } = await import('xlsx');


    // Convert JSON data to worksheet

    const worksheet = utils.json_to_sheet(records);

    const workbook = utils.book_new();

    utils.book_append_sheet(workbook, worksheet, 'Data');


    // Trigger native browser save dialog

    writeFile(workbook, fileName);

  };


  return (

    <button onClick={handleExport}>

      Download Spreadsheet

    </button>

  );

}


Implementation Checklist:

  • Buffer Types: Always use { type: 'buffer' } on the server and { type: 'array' } wrapped in a Uint8Array on the client.
  • Code Splitting: Use await import('xlsx') inside client event handlers to prevent bloating the initial bundle with spreadsheet parsing utilities.
  • Static Export Guard: Remember that generateStaticParams is strictly required only when generating dynamic routes under output: 'export'.


Important Note: By default, Excel stores date entries as raw sequential serial numbers. Always pass cellDates: true in your read options to ensure dates are automatically converted into native JavaScript Date objects.

Comments

Topics

Show more