Plenty of apps let users upload an Excel file — a price list, a contact import, a timesheet — and need to turn it into rows the backend understands. The hard part isn’t reading the file; it’s getting clean, typed data out of a messy real-world spreadsheet.
This is a practical walkthrough of that pipeline with ReoGrid: load the xlsx in the browser, detect the header, coerce types per column, handle multiple sheets, and POST the result. The file never leaves the client until you decide to send the cleaned data.
Want the full, lossless workbook (styles, formulas, merges) instead of plain data? That’s ReoGrid JSON — a different tool for a different job. This article is about extracting just the data.
The shape we want
A backend wants rows it can validate and insert — typically an array of objects:
[
{ "sku": "WID-1", "name": "Widget", "price": 9.99, "qty": 40 },
{ "sku": "GAD-2", "name": "Gadget", "price": 24.5, "qty": 12 }
]
Not styles, not formula sources — values, with the right types. Here’s how to get there.
Step 1 — load the file in the browser
import { createReogrid } from '@reogrid/pro';
const grid = createReogrid('#grid', { licenseKey: 'YOUR-LICENSE-KEY' });
const input = document.querySelector<HTMLInputElement>('#file')!;
input.addEventListener('change', async (e) => {
const file = (e.target as HTMLInputElement).files?.[0];
if (file) await grid.loadFromFile(file);
});
loadFromFile resolves once the file is fully parsed, so awaiting it means the data is ready to read.
Step 2 — read rows by header
The robust way to turn a sheet into objects: find the used extent, treat row 0 as the header, and read the displayed value of every cell.
import type { ReogridInstance } from '@reogrid/pro';
function sheetToRows(grid: ReogridInstance): Record<string, string>[] {
const ws = grid.worksheet;
const snapshot = ws.getExportSnapshot();
let lastRow = 0, lastCol = 0;
for (const cell of snapshot.cells) {
lastRow = Math.max(lastRow, cell.row);
lastCol = Math.max(lastCol, cell.column);
}
const headers: string[] = [];
for (let c = 0; c <= lastCol; c++) {
headers[c] = (ws.getDisplayText(0, c) || `col${c}`).trim();
}
const rows: Record<string, string>[] = [];
for (let r = 1; r <= lastRow; r++) {
const row: Record<string, string> = {};
for (let c = 0; c <= lastCol; c++) row[headers[c]] = ws.getDisplayText(r, c) ?? '';
rows.push(row);
}
return rows;
}
Use getDisplayText, not cell.value: it gives you the rendered value (formula results, formatted numbers), which is what a human — and your database — expects. Reading from getExportSnapshot().cells also means trailing empty rows and columns are excluded automatically.
Step 3 — map headers and coerce types
Real spreadsheets have headers like "Unit Price" and values like "$24.50". Map them to your schema and convert types explicitly:
interface Product {
sku: string;
name: string;
price: number;
qty: number;
}
const num = (s: string) => Number(s.replace(/[^0-9.\-]/g, '')); // strip $ , etc.
function toProducts(rows: Record<string, string>[]): Product[] {
return rows
.filter((r) => r['SKU']) // skip blank rows
.map((r) => ({
sku: r['SKU'].trim(),
name: r['Product Name'].trim(),
price: num(r['Unit Price']),
qty: num(r['Quantity']),
}));
}
Keeping coercion explicit (rather than guessing types) is what makes the import predictable. Validate here too — drop or flag rows that fail.
Step 4 — handle multiple sheets
A workbook often has more than one sheet. Iterate the workbook, switch the active sheet, and extract each:
function workbookToData(grid: ReogridInstance): Record<string, Record<string, string>[]> {
const out: Record<string, Record<string, string>[]> = {};
const wb = grid.workbook;
for (let i = 0; i < wb.sheetCount; i++) {
wb.setActiveSheet(i); // `grid.worksheet` now points at sheet i
out[grid.worksheet.name] = sheetToRows(grid);
}
return out;
}
// → { "Products": [...], "Suppliers": [...] }
Step 5 — send it
await grid.loadFromFile(file);
const products = toProducts(sheetToRows(grid));
const res = await fetch('/api/products/import', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(products),
});
if (!res.ok) throw new Error('Import failed');
Because parsing happened in the browser, your endpoint receives clean, validated JSON — not a raw file to wrangle. No server-side xlsx parser, no temp files.
Bonus: let users fix data before import
Since ReoGrid renders the file in an editable grid, you can show it and let the user correct typos or fill blanks before they hit “Import” — then run sheetToRows on their edited version. That single component does upload preview, editing, and extraction.
See also
- XLSX → JSON Converter demo — try it live, with the source
- Convert XLSX to Data Rows — the bare snippet
- Batch-process spreadsheet data with AI — send the JSON to an LLM
- Convert XLSX to JSON in the browser — lossless vs data JSON
- Bulk Load from JSON — the reverse direction