ReoGrid ReoGrid Web

From Excel to your app: extract structured data from xlsx as JSON

· unvell team
From Excel to your app: extract structured data from xlsx as JSON

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

Try ReoGrid Web in your project

Canvas-based Excel-compatible spreadsheet component for React and Vue. Lite is free — start with one npm install.

Related articles

Stay Updated

Be first to know — get updates as they ship

Get notified of new releases, features, and announcements.
No spam — just updates that matter.