24 KiB
title | sidebar_label | description | pagination_prev | pagination_next |
---|---|---|---|---|
Sheets in ReactJS Sites with NextJS | NextJS | Make static websites from spreadsheets using NextJS. Seamlessly integrate data into the data layer using SheetJS. Create content without leaving the comfort of Excel. | demos/net/index | demos/mobile/index |
import current from '/version.js'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import CodeBlock from '@theme/CodeBlock';
NextJS is a server-side framework for building static and dynamic sites using the ReactJS framework.
SheetJS is a JavaScript library for reading and writing data from spreadsheets.
This discussion covers three key SheetJS + NextJS operations:
-
Loading Data: NextJS can read files in lifecycle methods OR custom Webpack loaders can create asset modules.
-
Lifecycle Methods: NextJS includes strategies for static pages (
getStaticProps
) as well as dynamic pages (getServerSideProps
). -
Data Presentation: Pages use React and JSX.
The "Demo" uses NextJS and SheetJS to pull data from a spreadsheet. We'll explore how to create asset modules that process spreadsheet data at build time and how to read files on the server in NextJS lifecycle methods.
:::danger Telemetry
NextJS collects telemetry by default. The telemetry
subcommand can disable it:
npx -y next@13.5.6 telemetry disable
The setting can be verified by running
npx -y next@13.5.6 telemetry status
:::
:::info pass
This demo focuses on static sites where data files are processed at build time.
The ReactJS demo shows NextJS "Client Components".
:::
:::caution Next 13+ and SWC
Next 13 switched to the SWC minifier. There are known issues with the minifier.
Until those issues are resolved, SWC should be disabled in next.config.js
:
module.exports = {
// highlight-next-line
swcMinify: false
};
:::
:::note Tested Deployments
This demo was tested in the following environments:
NextJS | NodeJS | Date |
---|---|---|
9.5.5 |
16.20.2 |
2024-06-07 |
10.2.3 |
16.20.2 |
2024-06-07 |
11.1.4 |
16.20.2 |
2024-06-07 |
12.3.4 |
20.14.0 |
2024-06-07 |
13.5.6 |
20.14.0 |
2024-06-07 |
14.2.3 |
20.14.0 |
2024-06-07 |
:::
:::info pass
SheetJS libraries work in legacy NextJS apps. Older versions of this demo have
been tested against versions 3.2.3
, 4.2.3
, 5.1.0
, 6.1.2
and 7.0.3
.
NextJS has made a number of breaking changes over the years. Older versions of NextJS use legacy versions of ReactJS that do not support function components and other idioms.
examples/reactjs-legacy
on
the SheetJS Git server includes code samples for legacy NextJS versions.
:::
Loading Data
At a high level, there are two ways to pull spreadsheet data into NextJS apps: loading an asset module or performing the file read operations from the NextJS lifecycle methods.
Asset modules are appropriate for static sites when the file names are known in advance. Performing file read operations in lifecycle methods is more flexible but does not support live reloading.
Asset Module
:::caution pass
When the demo was last tested, Turbopack did not support true raw loaders. For
development use, the normal npx next dev
should be used.
:::
The SheetJS NodeJS module can be imported in Webpack asset modules1.
The following diagram depicts the workbook waltz:
In this flow, it is strongly recommended to make a loader return a Base64 string:
function loader(content) {
/* since `loader.raw` is true, `content` is a Buffer */
return `export default '${content.toString("base64")}'`;
}
/* ensure the function receives a Buffer */
loader.raw = true;
module.exports = loader;
The webpack configuration is controlled in next.config.js
:
module.exports = {
webpack: (config) => {
// highlight-start
/* add to the webpack config module.rules array */
config.module.rules.push({
/* `test` matches file extensions */
test: /\.(numbers|xls|xlsx|xlsb)/,
/* use the loader script */
use: [ { loader: './base64-loader' } ]
});
// highlight-end
return config;
}
};
Module alias directories can be defined in jsconfig.json
or tsconfig.json
:
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
// highlight-next-line
"@/*": ["*"]
}
}
}
Pages can import the files directly. It is strongly recommended to store files
in a data
folder.
In this example, the import statement pulls the sheetjs.xlsx
file as a Base64
string. The SheetJS read
method2 parses the string and returns a workbook
object3. The sheet_to_json
4 utility function generates an array of
objects based on the data. As long as the base64
variable is only used in
getStaticProps
, the library and file will be processed at build time.
import { read, utils } from 'xlsx';
// highlight-next-line
import base64 from '@/data/sheetjs.xlsx';
export async function getStaticProps() {
/* parse base64 data */
// highlight-next-line
const wb = read(base64, { type: "base64" });
return { props: {
/* generate array of objects from the first sheet */
data: utils.sheet_to_json(wb.Sheets[wb.SheetNames[0]])
} };
}
Raw Operations
The SheetJS NodeJS module can be imported from page scripts.
:::danger pass
The SheetJS ESM build
does not load NodeJS native modules directly. The Installation section includes
a note on dynamic import of fs
within lifecycle methods.
:::
Files can be read using the SheetJS readFile
5 method in lifecycle methods.
The cwd
method in the process
module will point to the root of the project.
The following diagram depicts the workbook waltz:
This example reads the file sheetjs.xlsx
in the data
folder in the project
and uses sheet_to_json
6 to generate data rows.
import { readFile, utils, set_fs } from 'xlsx';
import { join } from 'path';
import { cwd } from 'process';
export async function getServerSideProps() {
// highlight-start
set_fs(await import("fs")); // dynamically import 'fs' when needed
const filename = join(cwd(), "data", "sheetjs.xlsx"); // /data/sheetjs.xlsx
const wb = readFile(filename);
// highlight-end
/* generate and return the html from the first worksheet */
const data = utils.sheet_to_json(wb.Sheets[wb.SheetNames[0]]);
return { props: { data } };
}
:::danger Reading and writing files during the build process
As the NextJS workaround is non-traditional, it bears repeating:
fs
cannot be statically imported from the top level in NextJS pages. The
dynamic import must happen within a lifecycle function.
:::
NextJS Strategies
NextJS currently provides 3 strategies:
- "Static Site Generation" using
getStaticProps
7 - "SSG with Dynamic Routes" using
getStaticPaths
8 - "Server-Side Rendering" using
getServerSideProps
9
Static Site Generation
When using getStaticProps
, the file will be read once during build time.
This example reads sheetjs.xlsx
from the data
folder:
import { read, utils } from 'xlsx';
import base64 from '@/data/sheetjs.xlsx';
export async function getStaticProps() {
const wb = read(base64, { type: "base64" });
/* generate and return the html from the first worksheet */
const html = utils.sheet_to_html(wb.Sheets[wb.SheetNames[0]]);
return { props: { html } };
};
import { readFile, set_fs, utils } from 'xlsx';
import { join } from 'path';
import { cwd } from 'process';
export async function getStaticProps() {
set_fs(await import("fs"));
const filename = join(cwd(), "data", "sheetjs.xlsx"); // /data/sheetjs.xlsx
const wb = readFile(filename);
/* generate and return the html from the first worksheet */
const html = utils.sheet_to_html(wb.Sheets[wb.SheetNames[0]]);
return { props: { html } };
};
Dynamic Routes
Typically a static site with dynamic routes has an endpoint /sheets/[id]
that
implements both getStaticPaths
and getStaticProps
.
getStaticPaths
should return an array of worksheet indices:
import { read } from 'xlsx';
import base64 from '@/data/sheetjs.xlsx';
export async function getStaticPaths() {
/* read file */
const wb = read(base64, { type: "base64" });
/* generate an array of objects that will be used for generating pages */
const paths = wb.SheetNames.map((name, idx) => ({ params: { id: idx.toString() } }));
return { paths, fallback: false };
};
import { readFile, set_fs } from 'xlsx';
import { join } from 'path';
import { cwd } from 'process';
export async function getStaticPaths() {
/* read file */
set_fs(await import("fs"));
const filename = join(cwd(), "data", "sheetjs.xlsx"); // /data/sheetjs.xlsx
const wb = readFile(path);
/* generate an array of objects that will be used for generating pages */
const paths = wb.SheetNames.map((name, idx) => ({ params: { id: idx.toString() } }));
return { paths, fallback: false };
};
:::note pass
For a pure static site, fallback
must be set to false
10
:::
getStaticProps
will generate the actual HTML for each page:
import { read, utils } from 'xlsx';
import base64 from '@/data/sheetjs.xlsx';
export async function getStaticProps(ctx) {
/* read file */
const wb = read(base64, { type: "base64" });
/* get the corresponding worksheet and generate HTML */
const ws = wb.Sheets[wb.SheetNames[ctx.params.id]]; // id from getStaticPaths
const html = utils.sheet_to_html(ws);
return { props: { html } };
};
import { readFile, set_fs, utils } from 'xlsx';
import { join } from 'path';
import { cwd } from 'process';
export async function getStaticProps(ctx) {
/* read file */
set_fs(await import("fs"));
const filename = join(cwd(), "data", "sheetjs.xlsx"); // /data/sheetjs.xlsx
const wb = readFile(path);
/* get the corresponding worksheet and generate HTML */
const ws = wb.Sheets[wb.SheetNames[ctx.params.id]]; // id from getStaticPaths
const html = utils.sheet_to_html(ws);
return { props: { html } };
};
Server-Side Rendering
:::caution Do not use on a static site
These routes require a NodeJS dynamic server. Static page generation will fail!
getStaticProps
and getStaticPaths
support static site generation (SSG).
getServerSideProps
is suited for NodeJS hosted deployments where the workbook
changes frequently and a static site is undesirable.
:::
When using getServerSideProps
, the file will be read on each request.
:::caution Consider using a static strategy
When using asset modules, the file names and file paths are processed during the
build step. The content is fixed. In this situation, a static approach such as
getStaticProps
is strongly recommended.
:::
import { read } from 'xlsx';
import base64 from '@/data/sheetjs.xlsx';
export async function getServerSideProps() {
/* read file */
const wb = read(base64, { type: "base64" });
/* generate and return the html from the first worksheet */
const html = utils.sheet_to_html(wb.Sheets[wb.SheetNames[0]]);
return { props: { html } };
};
import { readFile, set_fs, utils } from 'xlsx';
import { join } from 'path';
import { cwd } from 'process';
export async function getServerSideProps() {
/* read file */
set_fs(await import("fs"));
const filename = join(cwd(), "data", "sheetjs.xlsx"); // /data/sheetjs.xlsx
const wb = readFile(path);
/* generate and return the html from the first worksheet */
const html = utils.sheet_to_html(wb.Sheets[wb.SheetNames[0]]);
return { props: { html } };
};
Data Presentation
The ReactJS demo compares common approaches.
HTML
HTML output can be generated using the SheetJS sheet_to_html
11 method and
inserted into the document using the dangerouslySetInnerHTML
12 attribute:
export default function Index({html, type}) { return (
<div dangerouslySetInnerHTML={{ __html: html }} />
); }
Arrays of Objects
Arrays of objects can be generated using the SheetJS sheet_to_json
13 method
and inserted into the document using standard JSX14:
export default function Index({aoo, type}) { return (
<table><thead><tr key={0}><th>Name</th><th>Index</th></tr></thead><tbody>
// highlight-start
{aoo.map(row => ( <tr>
<td>{row.Name}</td>
<td>{row.Index}</td>
</tr>))}
// highlight-end
</tbody></table>
); }
Demo
:::note pass
This demo showcases the following SheetJS + NextJS flows:
Page | Loading Data | Lifecycle Method | SheetJS API |
---|---|---|---|
/getStaticProps |
asset module | getStaticProps |
sheet_to_json |
/sheets/[id] |
asset module | getStaticPaths |
sheet_to_html |
/getServerSideProps |
lifecycle | getServerSideProps |
sheet_to_html |
The commands in this demo use next@13.5.6
. Other versions were tested by
replacing the version number in the relevant commands.
:::
:::caution pass
Older versions of NextJS will refuse to run in newer versions of NodeJS. The error message points to an issue with OpenSSL:
Error: error:0308010C:digital envelope routines::unsupported
When upgrading NextJS is not an option, NodeJS should be downgraded to v16.
:::
Initial Setup
- Disable NextJS telemetry:
npx -y next@13.5.6 telemetry disable
Confirm it is disabled by running
npx -y next@13.5.6 telemetry status
- Set up folder structure. At the end, a
pages
folder with asheets
subfolder must be created. On Linux or MacOS or WSL:
mkdir sheetjs-next
cd sheetjs-next
mkdir -p pages/sheets/
- Download the test file and place in the project root. On Linux or MacOS or WSL:
curl -LO https://docs.sheetjs.com/next/sheetjs.xlsx
- Install dependencies:
:::note pass
The next@13.5.6
depefndency can be adjusted to pick a different version. For
example, NextJS 12.3.4
is installed with
{\ npm i --save https://cdn.sheetjs.com/xlsx-${current}/xlsx-${current}.tgz next@12.3.4
}
:::
{\ npm i --save https://cdn.sheetjs.com/xlsx-${current}/xlsx-${current}.tgz next@13.5.6
}
- Download NextJS config scripts and place in the root folder:
On Linux or MacOS or WSL:
curl -LO https://docs.sheetjs.com/next/base64-loader.js
curl -LO https://docs.sheetjs.com/next/jsconfig.json
curl -LO https://docs.sheetjs.com/next/next.config.js
curl -LO https://docs.sheetjs.com/next/styles.css
- Download test scripts:
Download and place the following scripts in the pages
subfolder:
Download [id].js
and place in the
pages/sheets
subfolder.
:::caution Percent-Encoding in the script name
The [id].js
script must have the literal square brackets in the name. If your
browser saved the file to %5Bid%5D.js
. rename the file.
:::
On Linux or MacOS or WSL:
cd pages
curl -LO https://docs.sheetjs.com/next/_app.js
curl -LO https://docs.sheetjs.com/next/index.js
curl -LO https://docs.sheetjs.com/next/getServerSideProps.js
curl -LO https://docs.sheetjs.com/next/getStaticPaths.js
curl -LO https://docs.sheetjs.com/next/getStaticProps.js
cd sheets
curl -LOg 'https://docs.sheetjs.com/next/[id].js'
cd ../..
Testing
- Test the deployment:
npx next
Open a web browser and access:
http://localhost:3000
landing pagehttp://localhost:3000/getStaticProps
shows data from the first sheethttp://localhost:3000/getServerSideProps
shows data from the first sheethttp://localhost:3000/getStaticPaths
shows a list (2 sheets)
The individual worksheets are available at
http://localhost:3000/sheets/0
http://localhost:3000/sheets/1
- While the development server is running, open the
/getStaticProps
page and opensheetjs.xlsx
with a spreadsheet editor. In the editor, add a row to the bottom of the "Indices" worksheet (setA7
to "SheetJS Dev" andB7
to47
)
After saving the file, the website should refresh with the new row.
Production Build
- Stop the server and run a production build:
npx next build
The final output will show a list of the routes and types:
Route (pages) Size First Load JS
┌ ○ / 563 B 75.3 kB
├ /_app 0 B 74.8 kB
├ ○ /404 182 B 75 kB
├ λ /getServerSideProps 522 B 75.3 kB
├ ● /getStaticPaths 2.91 kB 77.7 kB
├ ● /getStaticProps 586 B 75.4 kB
└ ● /sheets/[id] (303 ms) 522 B 75.3 kB
├ /sheets/0
└ /sheets/1
As explained in the summary, the /getStaticPaths
and /getStaticProps
routes
are completely static. 2 /sheets/#
pages were generated, corresponding to 2
worksheets in the file. /getServerSideProps
is server-rendered.
:::info pass
NextJS historically used lowercase Lambda (λ
) to denote dynamic paths. This
was changed to a stylized lowercase F (ƒ
) in recent versions of NextJS.
:::
- Try to build a static site:
npx next export
:::danger NextJS breaking changes
NextJS 14 removed the export
subcommand!
:::
Edit next.config.js
and add the highlighted line:
module.exports = {
// highlight-next-line
output: "export",
webpack: (config) => {
After adding the line, run the build
command:
npx next build
This build will fail. A static page cannot be generated at this point because
/getServerSideProps
is server-rendered.
Static Site
- Delete
pages/getServerSideProps.js
:
rm -f pages/getServerSideProps.js
- Rebuild the static site:
Edit next.config.js
and comment the highlighted line:
module.exports = {
// highlight-next-line
// output: "export",
webpack: (config) => {
After editing next.config.js
, run the build command:
npx next build
Inspecting the output, there should be no lines with λ
or ƒ
:
Route (pages) Size First Load JS
┌ ○ / 563 B 75.3 kB
├ /_app 0 B 74.8 kB
├ ○ /404 182 B 75 kB
├ ● /getStaticPaths 2.91 kB 77.7 kB
├ ● /getStaticProps 586 B 75.4 kB
└ ● /sheets/[id] 522 B 75.3 kB
├ /sheets/0
└ /sheets/1
- Generate the static site:
npx next export
Edit next.config.js
and restore the highlighted line:
module.exports = {
// highlight-next-line
output: "export",
webpack: (config) => {
After adding the line, run the build
command:
npx next build
The static site will be written to the out
subfolder
- Serve the static site:
npx http-server out
The command will start a local HTTP server at http://localhost:8080/
for
testing the generated site. Note that /getServerSideProps
will 404 since the
page was removed.
-
See the "Webpack" asset module demo for more details. ↩︎
-
See "SheetJS Data Model" for more details. ↩︎
-
See
getStaticProps
in the NextJS documentation. ↩︎ -
See
getStaticPaths
in the NextJS documentation. ↩︎ -
See
getServerSideProps
in the NextJS documentation. ↩︎ -
See
fallback
in getStaticPaths in the NextJS documentation. ↩︎ -
dangerouslySetInnerHTML
is a ReactJS prop supported for all built-in components. ↩︎