328 lines
8.5 KiB
Markdown
328 lines
8.5 KiB
Markdown
|
---
|
||
|
title: NextJS
|
||
|
pagination_prev: demos/extensions/index
|
||
|
pagination_next: demos/gsheet
|
||
|
---
|
||
|
|
||
|
:::note
|
||
|
|
||
|
This was tested against `next v13.1.1` on 2023 January 14.
|
||
|
|
||
|
:::
|
||
|
|
||
|
:::info
|
||
|
|
||
|
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. At the time of writing, NextJS does not offer an out-of-the-box
|
||
|
asset module solution, so this demo focuses on raw operations. NextJS does not
|
||
|
watch the spreadsheets, so `next dev` hot reloading will not work!
|
||
|
|
||
|
:::
|
||
|
|
||
|
The general strategy with NextJS apps is to generate HTML snippets or data from
|
||
|
the lifecycle functions and reference them in the template.
|
||
|
|
||
|
HTML output can be generated using `XLSX.utils.sheet_to_html` and inserted into
|
||
|
the document using the `dangerouslySetInnerHTML` attribute:
|
||
|
|
||
|
```jsx
|
||
|
export default function Index({html, type}) { return (
|
||
|
// ...
|
||
|
// highlight-next-line
|
||
|
<div dangerouslySetInnerHTML={{ __html: html }} />
|
||
|
// ...
|
||
|
); }
|
||
|
```
|
||
|
|
||
|
:::warning Reading and writing files during the build process
|
||
|
|
||
|
`fs` cannot be statically imported from the top level in NextJS pages. The
|
||
|
dynamic import must happen within a lifecycle function. For example:
|
||
|
|
||
|
```js
|
||
|
/* it is safe to import the library from the top level */
|
||
|
import { readFile, utils, set_fs } from 'xlsx';
|
||
|
/* it is not safe to import 'fs' from the top level ! */
|
||
|
// import * as fs from 'fs'; // this will fail
|
||
|
import { join } from 'path';
|
||
|
import { cwd } from 'process';
|
||
|
|
||
|
export async function getServerSideProps() {
|
||
|
// highlight-next-line
|
||
|
set_fs(await import("fs")); // dynamically import 'fs' when needed
|
||
|
const wb = readFile(join(cwd(), "public", "sheetjs.xlsx")); // works
|
||
|
// ...
|
||
|
}
|
||
|
```
|
||
|
|
||
|
:::
|
||
|
|
||
|
:::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`:
|
||
|
|
||
|
```js title="next.config.js"
|
||
|
module.exports = {
|
||
|
// highlight-next-line
|
||
|
swcMinify: false
|
||
|
};
|
||
|
```
|
||
|
|
||
|
:::
|
||
|
|
||
|
## NextJS Strategies
|
||
|
|
||
|
NextJS currently provides 3 strategies:
|
||
|
|
||
|
- "Static Site Generation" using `getStaticProps`
|
||
|
- "SSG with Dynamic Routes" using `getStaticPaths`
|
||
|
- "Server-Side Rendering" using `getServerSideProps`
|
||
|
|
||
|
### Static Site Generation
|
||
|
|
||
|
When using `getStaticProps`, the file will be read once during build time.
|
||
|
|
||
|
```js
|
||
|
import { readFile, set_fs, utils } from 'xlsx';
|
||
|
|
||
|
export async function getStaticProps() {
|
||
|
/* read file */
|
||
|
set_fs(await import("fs"));
|
||
|
const wb = readFile(path_to_file)
|
||
|
|
||
|
/* 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:
|
||
|
|
||
|
```js
|
||
|
export async function getStaticPaths() {
|
||
|
/* read file */
|
||
|
set_fs(await import("fs"));
|
||
|
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
|
||
|
|
||
|
For a pure static site, `fallback` must be set to `false`!
|
||
|
|
||
|
:::
|
||
|
|
||
|
- `getStaticProps` will generate the actual HTML for each page:
|
||
|
|
||
|
```js
|
||
|
export async function getStaticProps(ctx) {
|
||
|
/* read file */
|
||
|
set_fs(await import("fs"));
|
||
|
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.
|
||
|
|
||
|
```js
|
||
|
import { readFile, set_fs, utils } from 'xlsx';
|
||
|
|
||
|
export async function getServerSideProps() {
|
||
|
/* read file */
|
||
|
set_fs(await import("fs"));
|
||
|
const wb = readFile(path_to_file);
|
||
|
|
||
|
/* generate and return the html from the first worksheet */
|
||
|
const html = utils.sheet_to_html(wb.Sheets[wb.SheetNames[0]]);
|
||
|
return { props: { html } };
|
||
|
};
|
||
|
```
|
||
|
|
||
|
## Demo
|
||
|
|
||
|
0) Disable NextJS telemetry:
|
||
|
|
||
|
```js
|
||
|
npx next@13.1.1 telemetry disable
|
||
|
```
|
||
|
|
||
|
Confirm it is disabled by running
|
||
|
|
||
|
```js
|
||
|
npx next@13.1.1 telemetry status
|
||
|
```
|
||
|
|
||
|
1) Set up folder structure. At the end, a `pages` folder with a `sheets`
|
||
|
subfolder must be created. On Linux or MacOS or WSL:
|
||
|
|
||
|
```bash
|
||
|
mkdir -p pages/sheets/
|
||
|
```
|
||
|
|
||
|
2) Download the [test file](pathname:///next/sheetjs.xlsx) and place in the
|
||
|
project root. On Linux or MacOS or WSL:
|
||
|
|
||
|
```bash
|
||
|
curl -LO https://docs.sheetjs.com/next/sheetjs.xlsx
|
||
|
```
|
||
|
|
||
|
3) Install dependencies:
|
||
|
|
||
|
```bash
|
||
|
npm i --save https://cdn.sheetjs.com/xlsx-latest/xlsx-latest.tgz next@13.1.1
|
||
|
```
|
||
|
|
||
|
4) Download test scripts:
|
||
|
|
||
|
Download and place the following scripts in the `pages` subfolder:
|
||
|
|
||
|
- [`index.js`](pathname:///next/index.js)
|
||
|
- [`getServerSideProps.js`](pathname:///next/getServerSideProps.js)
|
||
|
- [`getStaticPaths.js`](pathname:///next/getStaticPaths.js)
|
||
|
- [`getStaticProps.js`](pathname:///next/getStaticProps.js)
|
||
|
|
||
|
Download [`[id].js`](pathname:///next/%5Bid%5D.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:
|
||
|
|
||
|
```bash
|
||
|
cd pages
|
||
|
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 ../..
|
||
|
```
|
||
|
|
||
|
5) Test the deployment:
|
||
|
|
||
|
```bash
|
||
|
npx next@13.1.1
|
||
|
```
|
||
|
|
||
|
Open a web browser and access:
|
||
|
|
||
|
- http://localhost:3000 landing page
|
||
|
- http://localhost:3000/getStaticProps shows data from the first sheet
|
||
|
- http://localhost:3000/getServerSideProps shows data from the first sheet
|
||
|
- http://localhost:3000/getStaticPaths shows a list (3 sheets)
|
||
|
|
||
|
The individual worksheets are available at
|
||
|
|
||
|
- http://localhost:3000/sheets/0
|
||
|
- http://localhost:3000/sheets/1
|
||
|
- http://localhost:3000/sheets/2
|
||
|
|
||
|
6) Stop the server and run a production build:
|
||
|
|
||
|
```bash
|
||
|
npx next@13.1.1 build
|
||
|
```
|
||
|
|
||
|
The final output will show a list of the routes and types:
|
||
|
|
||
|
```
|
||
|
Route (pages) Size First Load JS
|
||
|
┌ ○ / 541 B 77.4 kB
|
||
|
├ ○ /404 181 B 73.7 kB
|
||
|
├ λ /getServerSideProps 594 B 77.4 kB
|
||
|
├ ● /getStaticPaths 2.56 kB 79.4 kB
|
||
|
├ ● /getStaticProps 591 B 77.4 kB
|
||
|
└ ● /sheets/[id] (447 ms) 569 B 77.4 kB
|
||
|
├ /sheets/0
|
||
|
├ /sheets/1
|
||
|
└ /sheets/2
|
||
|
```
|
||
|
|
||
|
As explained in the summary, the `/getStaticPaths` and `/getStaticProps` routes
|
||
|
are completely static. 3 `/sheets/#` pages were generated, corresponding to 3
|
||
|
worksheets in the file. `/getServerSideProps` is server-rendered.
|
||
|
|
||
|
7) Try to build a static site:
|
||
|
|
||
|
```bash
|
||
|
npx next@13.1.1 export
|
||
|
```
|
||
|
|
||
|
:::note The static export will fail!
|
||
|
|
||
|
A static page cannot be generated at this point because `/getServerSideProps`
|
||
|
is still server-rendered.
|
||
|
|
||
|
:::
|
||
|
|
||
|
8) Delete `pages/getServerSideProps.js` and rebuild:
|
||
|
|
||
|
```bash
|
||
|
rm -f pages/getServerSideProps.js
|
||
|
npx next@13.1.1 build
|
||
|
```
|
||
|
|
||
|
Inspecting the output, there should be no lines with the `λ` symbol:
|
||
|
|
||
|
```
|
||
|
Route (pages) Size First Load JS
|
||
|
┌ ○ / 541 B 77.4 kB
|
||
|
├ ○ /404 181 B 73.7 kB
|
||
|
├ ● /getStaticPaths 2.56 kB 79.4 kB
|
||
|
├ ● /getStaticProps 591 B 77.4 kB
|
||
|
└ ● /sheets/[id] (459 ms) 569 B 77.4 kB
|
||
|
├ /sheets/0
|
||
|
├ /sheets/1
|
||
|
└ /sheets/2
|
||
|
```
|
||
|
|
||
|
9) Generate the static site:
|
||
|
|
||
|
```bash
|
||
|
npx next@13.1.1 export
|
||
|
```
|
||
|
|
||
|
The static site will be written to the `out` subfolder, which can be hosted with
|
||
|
|
||
|
```bash
|
||
|
npx http-server out
|
||
|
```
|
||
|
|
||
|
The command will start a local HTTP server for testing the generated site. Note
|
||
|
that `/getServerSideProps` will 404 since the page was removed.
|