Codegen utility to split page using react-router-dom
Split Pages CLI
Folder-based Type-safe Router Generator
Installation
# npm
npm install @monoid-dev/split-pages
# yarn
yarn add @monoid-dev/split-pages
Introduction
Prerequisites: React, React Router V5, TypeScript. This guide also assumes yarn
to be the package manager.
split-page is a tool that builds your navigation based on your file system. To get started, create a pages.config.js
file at the root of your project:
/**
* @type {import('@monoid-dev/split-pages').SplitPagesInputOptions}
*/
module.exports = {
pageRoot: './src/pages',
};
You probably want to add .split-pages/
to your .gitignore
, too. This is where we put the auto-generated code by default.
Now, create 3 pages A
, B
and NoMatch
under ./src/pages
, populating them with some content:
src/pages
├── A.tsx
├── B.tsx
└── NoMatch.tsx
// A.tsx
import React from 'react';
import { definePage } from '@monoid-dev/split-pages/client';
export const A = definePage({}, (props) => {
return <>A</>;
});
// B.tsx
import React from 'react';
import { definePage } from '@monoid-dev/split-pages/client';
export const B = definePage({}, (props) => {
return <>B</>;
});
// NoMatch.tsx
import React from 'react';
import { definePage } from '@monoid-dev/split-pages/client';
export const NoMatch = definePage({}, (props) => {
return <>NoMatch</>;
});
Now you are ready to rock ~ run the command to generate your own type-safe react-router@5
navigation!
yarn split-pages build
Now you’ll notice a new folder born to your project: .split-pages
. It provides you with a default export SplitPagesIndex
, and that’s the navigation root.
Now you have App.tsx
as your default entry, put that default export to it:
import React from 'react';
import { BrowserRouter, Link } from 'react-router-dom';
import SplitPagesIndex from '../.split-pages';
export function App() {
return (
<BrowserRouter>
<nav>
<Link to={'/A'}>A</Link>
<Link to={'/B'}>B</Link>
</nav>
<SplitPagesIndex />
</BrowserRouter>
);
}
It’s natively react-router-dom
, but you don’t have to define the router by hand, instead it follows your file system.
Configuration Reference
Currently, the user configuration is defined as a SplitPagesInputOptions
interface. You can import that by:
import { SplitPagesInputOptions } from '@monoid-dev/split-pages'
SplitPagesInputOptions.pageRoot: string
REQUIRED. The folder where you put your pages. The folder can be arbitrarily nested, and the file system path relative to the root will be directly mapped to the URL path, chopping off the extension:
./src/pages/path/to/your/Page.tsx
->
http://localhost:3000/path/to/your/Page
To add a page, you need to follow the conventions:
-
Export the same name component as your file name. It is treated as the page component.
-
Use
definePage
to create the component. This will have you some advantages to helpsplit-pages
exploit TypeScript.
SplitPagesInputOptions.chunkPrefixes?: string[]
OPTIONAL. A list of prefixes that you wish each of them behind an asynchronous import()
respectively. This will guide the bundler to put only the pages with those prefixes into separate chunks. For example, you might have
{
chunkPrefixes: ['/admin']
}
This will put all /admin
pages into a separate chunk, and other pages into another chunk. When customers login, they don’t download the entire /admin
chunk.
SplitPagesInputOptions.outDir?: string
OPTIONAL. The output directory where we put the generated TypeScript code. By default, it is treated as .split-pages
. You probably want to add your <outDir>/
to your .gitignore
.
SplitPagesInputOptions.redirections?: { [path in string]: string; }
OPTIONAL. Each key value pair means “if we have the key as the pathname, we redirect to the value. “.
SplitPagesInputOptions.containerModule?: string
OPTIONAL. The component that you want to wrap each of your page component. Might be something you want to share across all of your pages: a navigation bar, an error boundary, or something else.
The value should be a path relative to your project root to the file that export default
s your container component.
Due to how
react-router@5
works, the component won’t unmount/mount during page navigation, it is either possibly the desired behavior or not.
API Reference
This part will introduce the browser-side APIs that we provide. They are defined in @monoid-dev/split-pages/client
or generated in .split-pages/meta
.
definePage
You should use definePage
to wrap each of your page component.
Example:
import React from 'react';
import { definePage } from '@monoid-dev/split-pages/client';
export const A = definePage({}, (props) => {
return <>A</>;
});
Usage:
- Define the type of the query.
To do this, simply provide a struct
schema to the props
field of the first parameter:
import React from 'react';
import { definePage } from '@monoid-dev/split-pages/client';
import { numberField, struct } from '@monoid-dev/reform';
export const A = definePage(
{
props: struct({
a: numberField(),
}),
},
({ a }) => {
return <>{a}</>;
},
);
Here, we introduce @monoid-dev/reform, a runtime type checking and conversion library. It is similar to io-ts
, joi
or yup
, but more satisfies our needs. Here we ask the URL for page A
to support queries like ?a=string
, and a non-numeric a
parameter will result in a thrown error (you probably want to catch that through a componentDidCatch
). The arguments are passed to the function where you define the page component, and you can render that to your page.
url
url
provides a type safe way to build urls. url
takes the first parameter as the pathname, and the second parameter as the query. Thanks to TypeScript, you CAN’T provide unchecked queries.
Example:
import { url } from '../.split-pages/meta';
url('/A', { a: 123 })
url('/B', {})
AppUrl
import type { AppUrl } from '../.split-pages/meta';
AppUrl
is the union of all possible paths in your app. It is useful if you want to add additional metadata to each page:
import type { AppUrl } from '../.split-pages/meta';
const pageTitles: { [K in AppUrl]: string } = {
'/A': 'A',
'/B': 'B',
};
Caveats
-
There are no official ways to support nested routing. If you want to implement that, you have to do that with native react-router.
-
You can’t define runtime metadata at each page. Instead, grap the
AppUrl
union from the genereted data and gather them into the same module. This is because we cannot import those data without importing the entire page, if we want full JavaScript flexibility. -
No support for react-router other than v5. React router introduces huge incompatibility between major versions and following that is not one of our interests.
-
No support for pure JavaScript. TypeScript is REQUIRED. If your application scales to the size that you need auto navigation generation, you are probably already using TypeScript.
Acknowledgements
Thanks to the guys in Monoid to be the first users of this library.