「自分の Excel ファイルをそのままドロップさせてくれない?」と何度も聞かれる Web アプリのカテゴリがあります。社内ツール、レポートダッシュボード、データインポートウィザード、価格表アップローダなど。これに対する正直な回答は、たいていの場合「サーバーに送って、パースして、再描画する」というものでした。5MB のファイルをバックエンド経由で往復させて、それをユーザーに表示し直すためだけに、可動部品が多すぎます。
ReoGrid Web は、このパイプライン全体をブラウザ内で完結させます。ファイルピッカーまたはドラッグ&ドロップ → ブラウザ内パース → Canvas 描画。ユーザーはネットワークリクエストを一切目にしません。セルのスタイル、結合、罫線、ウィンドウ枠固定、数式、条件付き書式、アウトライングルーピングのすべてが保持されます。
この記事では、その実装方法、ラウンドトリップで何が維持されるか、そして大きなファイルでタブをフリーズさせない方法を解説します。
30 秒で動かす版
<input type="file" accept=".xlsx" id="file" />
<div id="grid" style="width: 100%; height: 600px;"></div>
import { createReogrid } from '@reogrid/lite';
const { worksheet } = createReogrid('#grid');
document.querySelector<HTMLInputElement>('#file')!
.addEventListener('change', async (e) => {
const file = (e.target as HTMLInputElement).files?.[0];
if (!file) return;
await worksheet.loadFromFile(file);
});
これだけです。loadFromFile は File オブジェクトを直接受け取り、FileReader でバイト列を読み、OOXML zip をパースし、セル/スタイル/結合/罫線を物質化して、グリッドが表示可能になった時点で解決します。
注意: xlsx インポートは Lite でも Pro でも動作します。 エクスポートのみが Pro 限定です。つまり、このコードは無料の Lite パッケージで動きます。
React でドラッグ&ドロップ対応
実際のアプリでは、ファイル入力よりもドロップゾーンの方が UX として自然です。ファイル入力とドラッグ&ドロップフォールバックを併用した完全パターンがこちらです。
import { Reogrid, type ReogridInstance } from '@reogrid/lite/react';
import { useRef, useState, type DragEvent } from 'react';
export default function XlsxViewer() {
const gridRef = useRef<ReogridInstance>(null);
const [info, setInfo] = useState('ファイル未読み込み');
const [dragging, setDragging] = useState(false);
async function loadFile(file: File) {
if (!file.name.endsWith('.xlsx')) {
setInfo('.xlsx ファイルをドロップしてください');
return;
}
await gridRef.current?.worksheet.loadFromFile(file);
setInfo(`${file.name} · ${(file.size / 1024).toFixed(1)} KB`);
}
function onDrop(e: DragEvent) {
e.preventDefault();
setDragging(false);
const file = e.dataTransfer.files[0];
if (file) loadFile(file);
}
return (
<div
onDragEnter={(e) => { e.preventDefault(); setDragging(true); }}
onDragLeave={(e) => { e.preventDefault(); setDragging(false); }}
onDragOver={(e) => e.preventDefault()}
onDrop={onDrop}
style={{ position: 'relative' }}
>
<input
type="file"
accept=".xlsx"
onChange={(e) => e.target.files?.[0] && loadFile(e.target.files[0])}
/>
<div style={{ marginTop: 4, fontSize: 13, color: '#64748b' }}>{info}</div>
<Reogrid ref={gridRef} style={{ width: '100%', height: 600 }} />
{dragging && (
<div style={{
position: 'absolute', inset: 0, background: 'rgba(59,130,246,0.1)',
border: '2px dashed #3b82f6', borderRadius: 8,
display: 'flex', alignItems: 'center', justifyContent: 'center',
fontSize: 16, fontWeight: 600, color: '#1d4ed8', pointerEvents: 'none',
}}>
ドロップして読み込む
</div>
)}
</div>
);
}
本サイトの XLSX ビューアデモ は実質的にこのコードで動いています。ご自身のファイルをドロップして試してみてください — マシンから外には何も出ません。
ラウンドトリップで何が維持されるか
実際の .xlsx ファイルは見た目以上にやっかいです。フォーマット仕様としては OOXML ですが、現実には「Excel がたまたまそう書いたもの」が支配的で、テーマカラー、共有文字列、dxf による上書き、リッチテキストラン、表示形式の条件節などが混在しています。ReoGrid Web のインポーターは v1.1〜v1.2 を通じて、現実のサーフェスに対して堅牢化されてきました。
| 機能 | 状況 |
|---|---|
| セル値、数式、共有文字列 | ✅ |
| セルスタイル(フォント、色、配置、塗りつぶし) | ✅ |
| 罫線(辺ごと、色、スタイル) | ✅ |
| 結合セル | ✅ |
| 表示形式(通貨、日付、カスタムパターン) | ✅ — [=0]"-";m/d/yy のような条件節も対応 |
| ウィンドウ枠固定 | ✅ — エクスポート時もラウンドトリップ |
| 行/列アウトライン(グルーピング) | ✅ — レベル、折りたたみ状態、サマリ方向 |
| 条件付き書式 | ✅ — テーマカラー+tint や <bgColor> 塗りつぶしを含む |
| 画像 | ✅ |
| リッチテキストラン | ✅ |
| ピボットテーブル | ❌ — フラットなセル値として読み込み |
| グラフ | ❌ |
| マクロ (VBA) | ❌ — 無視 |
v1.2 で入った修正のうち、実ファイルに影響するものを 2 つ紹介します。
- Excel シリアル日付のオフバイワンを修正。 シリアル 45658 が正しく 2025-01-01 に解決されるようになりました。古い Excel で保存され、1900 年うるう年バグを誤って扱うライブラリでラウンドトリップしたファイルでは、誤った日付になっていました。
- 表示形式の条件節(
[=0]、[<>N]、[<N]、[<=N]、[>N]、[>=N])が値の型に依存せず評価されるようになりました。[=0]"-";m/d/yyのような書式コードが常に第 1 節を選んでしまう問題が解消しました。
正しく描画されないファイルがあれば、ぜひお知らせください。
大きなファイル: タブをフリーズさせない
44 万セルのファイルを同期的にロードするとメインスレッドを数秒間ブロックします。ユーザーにはフリーズしたタブが見えてしまいます。これはチャンク式ローダーで完全に回避できます。
await worksheet.loadFromFile(file, { chunked: true });
このオプションの動作:
- OOXML zip を 1 パスでパースする(ここはセル数ではなくファイルサイズに支配される)。
- セルの取り込みバッチ間で、
requestAnimationFrameを介して メインスレッドを譲る。 - 数秒のフリーズではなく、約 40ms で最初のフレームを描画し、ロードが進むにつれて行が埋まっていく。
バッチサイズはオプションで調整できます。
await worksheet.loadFromFile(file, { chunked: { batchSize: 2000 } });
ベンチマークファイル(1945 × 503、約 44 万セル)でのエンドツーエンドのロード時間:
| 同期 | チャンク | |
|---|---|---|
| 初回描画 | 約 3.7s | 約 40ms |
| 総ロード時間 | 3.7s | 約 4.0s |
| メインスレッドのフリーズ | 3.7s | なし — 16〜30ms のスライス |
チャンク化による小さなオーバーヘッド(合計で約 10% 遅くなる)はありますが、5 万セル以上のファイルでは UX のトレードオフとして十分に価値があります。
なお v1.1 では同期ロード自体も約 40% 高速化されました(同じベンチマークで 6.2s → 3.7s)。パーサ、セル抽出ウォーク、ワークシートのバルクロードパス、数式エンジンの初回再構築のそれぞれにわたる改善です。
代替のソース
loadFromFile は最も扱いやすいパスです。File オブジェクト以外から読み込みたい場合は、低レベル API が便利です。
// 自オリジン上の URL から
await worksheet.loadFromUrl('/templates/budget.xlsx');
// 生の ArrayBuffer から(fetch 済み、IndexedDB からのストリームなど)
const buf = await fetch('/api/report').then(r => r.arrayBuffer());
await worksheet.loadFromBuffer(buf);
// 特定のシートを指定
await worksheet.loadXlsx(buf, { sheetName: 'Q2 Forecast' });
// チャンク版も同じ
await worksheet.loadFromUrl('/large.xlsx', { chunked: true });
Lite ティアの制限
Lite エディションは 100 行 × 26 列を超えるデータを暗黙的に切り詰めます。未知のユーザーファイルを扱う汎用 xlsx ビューアを構築する場合は、Pro エディションでこの制限を解除(加えて 109 関数の数式ライブラリと xlsx エクスポート機能を追加)できます。マトリックスは 料金ページ を参照してください。
これが重要な理由
バックエンドを介したアップロード → パース → 描画のループは、数 MB の HTTP リクエスト、サーバー側のパース CPU、一時ファイルのライフサイクル、そしてラウンドトリップが完了するまで何も表示できない UI を強いてきます。ビューアやクイックインポートウィザードの場合、それらはユーザーが望む仕事をしていません。
パーサーをブラウザに移すことで、ファイルはデバイスから出ず、ロード時間はユーザーマシンの CPU だけに制限され、UI は段階的に描画を始められます。ユーザーがファイルをクリックすると、自分のスプレッドシートが見える — それがインタラクションのすべてです。
ご自身の .xlsx ファイルを XLSX ビューアデモ にドロップして試してみてください。シリーズの次回は、数式エンジンの上に何を構築できるかを掘り下げます — 編集可能な数量、自動の行小計、税、合計、すべて数式で動く完成度の高い React 製インボイスです。