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.
Select any stage to inspect the execution environment, SheetJS API methods, and output formats.
Ingestion
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
Post a Comment
Oof!