Routes
Shuvi provides a file system-based convention routing rule.
Read the agreed files from src/routes in the project root directory,
such as page.js, layout.js
and api.js
.
Which produces the corresponding React Component
routing hierarchy.
Generate routing paths
The names of the directories under src/routes determine the rules for generating routes
Example:
src
└── routes
├── page.jsx ───────────────── /
├── categories
│ ├── page.jsx ───────────── /categories
│ ├── $categoryId
│ │ └── page.jsx ───────── /categories/:categoryId
├── articles
│ ├── page.jsx ───────────── /articles
│ ├── $articleId
│ │ ├── page.jsx ───────── /articles/:articleId
│ │ └── $commentId
│ │ └── page.jsx ───── /articles/:articleId/:commentId
│ ├── layout.jsx
├── tags
│ ├── page.jsx ───────────── /tags
├── setting
│ ├── page.jsx ───────────── /setting
│ ├── accountInfo
│ │ └── page.jsx ───────── /setting/accountInfo
│ ├── preference
│ │ └── page.jsx ───────── /setting/preference
│ ├── others
│ │ └── page.jsx ───────── /setting/others
│ │ └── $
│ │ └── page.jsx ───── /setting/others/*
│ └── layout.jsx
└── layout.jsx
Generate dynamic paths
Defining routes by using predefined paths is not always enough for complex applications.
In shuvi you can add brackets to a directory name ($param
) to create a dynamic route (a.k.a. url slugs, pretty urls, and others).
Consider the following directory routes/post/$postId
:
Any route like /post/traveling
, /post/hiking
, etc. will be matched by routes/post/$postId
.
For example, the route /post/traveling
will have the following params
object:
{ "postId": "traveling" }
Similarly, the route /post/traveling?language=en
will have the following params
and query
object:
// params
{"postId": "traveling"}
// query
{ "language": "en" }
Multiple dynamic route segments work the same way. The page routes/post/$postId/$commentId
will match the route /post/traveling/randomCommentId
and its params
object will be:
{ "postId": "traveling", "commentId": "randomCommentId" }
Splats
Dynamic routes could deal with any URL by naming a fold $
. It will match the value in the rest of URL to the end.
routes/setting/others/$
matches/setting/others
, but also/setting/others/background
,/setting/others/background/color
,/setting/others/resolution
and so on.
You could get matched params in the loader function
import { Loader } from "@shuvi/runtime";
export const loader: Loader = async (ctx) => {
ctx.params["*"]; // "" or "background" or "background/color"
};
export default App;
or using the hook revealed from shuvi
out of the box
import { useParams } from "@shuvi/runtime";
function App() {
const params = useParams();
params["*"]; // "" or "background" or "background/color"
}
export default App;
General rules
path | route | matched url |
---|---|---|
routes/tags | /tags | /tags |
routes/articles/$articleId | /articles/:articleId | /articles/traveling, /articles/hiking |
routes/setting/others/$ | /setting/others/* | /setting/others, /setting/others/any-routes, /setting/others/any-routes/any/routes |
[details about matched rules](#Match rules)
Generate routing endpoints
Page routes
The directory name determines the path, page.js determines the rendered content.
page.js
should export a React component
by default.page.js is leaf node in the routing tree,
Can be nested in any folder.
Note that all future examples will use the .js extension. The extension of the routing file is not limited to
.js
, but can also be.ts
,.tsx
, and.jsx
.
Usage
- Create a
page.js
file under yourroutes
directory. - Finally, export a React component function from the
page.js
file by default.
Example
export default function Index() {
return <div>index</div>;
}
export default function Index() {
return <div>a-b</div>;
}
Layout routes
Layout is suitable for scenarios that require nested routing.
Similar to the <router-view>
of Vue-router
and <Outlet>
of Remix
Layout can be understood as a more advanced page, it has all the capabilities of page, and has the ability to share areas without repeated rendering and scheduling of sub-routes.
Usage
- Create a
layout.js
file under yourroutes
directory. - Finally, export a React component function from the
layout.js
file by default. - Use 'RouterView Component' to render your child routes
Example
A small example of a shared top navigation bar.
src/routes/dashboard/layout.js
→ /dashboard
import { RouterView } from "@shuvi/runtime";
export default function Layout() {
return (
<div>
<header>
<h1>dashboard</h1>
<nav>
<Link to="categories">categories</Link>
<Link to="articles">articles</Link>
<Link to="tags">tags</Link>
</nav>
</header>
<main>
<RouterView />
</main>
</div>
);
}
src/routes/dashboard/categories/page.js
→ /dashboard/categories
export default function () {
return <div>categories</div>;
}
src/routes/dashboard/articles/page.js
→ /dashboard/articles
export default function () {
return <div>articles</div>;
}
src/routes/dashboard/tags/page.js
→ /dashboard/tags
export default function () {
return <div>tags</div>;
}
src/routes/dashboard/page.js
→ /dashboard
can be used as index route.
export default function () {
return <div>index</div>;
}
Now, accessing any sub-route under /dashboard/ will render the matching layout and page together. When the route changes, the layout will not be unmounted. Implemented regional component sharing
Notice: Layout and page do not conflict with dynamic segment and can be freely combined.
API routes
API routes provide a solution to build your API with shuvi.
Any file inside the folder src/routes
, each api is associated with a route based on its file name. They are server-side only bundles and won't increase your client-side bundle size.
path | route | matched url |
---|---|---|
routes/api/post/create/api.js | /api/post/create | /api/post/create |
routes/api/post/$pid/api.js | /api/post/:pid | /api/post/1, /api/post/abc |
routes/api/post/$/api.js | /api/post/* | /api/post, /api/post/1, /api/post/1/2, /api/post/abc/other/path |
Usage
- Create a
api.js
file under yourroutes
directory. - Finally, export an api function from the
api.js
file by default.
Example
For example, the following API route routes/api/user/api.js
returns a json
response with a status code of 200
:
export default function handler(req, res) {
res.status(200).json({ name: "John Doe" });
}
For an API route to work, you need to export a function as default (a.k.a request handler), which then receives the following parameters:
req
: An instance of http.IncomingMessage, plus some enhanced requestres
: An instance of http.ServerResponse, plus some enhanced response
To handle different HTTP methods in an API route, you can use req.method
in your request handler, like so:
import { RuntimeServer } from "@shuvi/runtime";
const apiHandler: RuntimeServer.IApiRequestHandler = function handler(
req,
res
) {
if (req.method === "POST") {
// Process a POST request
} else {
// Handle any other HTTP method
}
};
export default apiHandler;
Details of
RuntimeServer.IApiRequestHandler
types is here
To fetch API endpoints, take a look into any of the examples at the start of this section.
enhanced request
API routes provide built in middlewares which parse the incoming request (req
). Those middlewares are:
req.cookies
- An object containing the cookies sent by the request. Defaults to{}
req.query
- An object containing the query string. Defaults to{}
req.body
- An object containing the body parsed bycontent-type
, ornull
if no body was sent
Custom config
The apiConfig
object includes all configs available for API routes.
Every API route can export a config
object to change the default configs, which are the following:
export const config = {
apiConfig: {
bodyParser: {
sizeLimit: "1mb",
},
},
};
bodyParser
Enables body parsing, you can disable it if you want to consume it as a Stream
:
export const config = {
apiConfig: {
bodyParser: false,
},
};
bodyParser.sizeLimit
is the maximum size allowed for the parsed body, in any format supported by bytes, like so:
export const config = {
apiConfig: {
bodyParser: {
sizeLimit: "500kb",
},
},
};
enhanced response
The response (res
) includes a set of Express.js-like methods to improve the developer experience and increase the speed of creating new API endpoints, take a look at the following example:
export default function handler(req, res) {
res.status(200).json({ name: "shuvi" });
}
The included helpers are:
res.status(code)
- A function to set the status code.code
must be a valid HTTP status coderes.json(body)
- Sends a JSON response.body
must be a serialiazable objectres.send(body)
- Sends the HTTP response.body
can be astring
, anobject
or aBuffer
res.redirect([status,] path)
- Redirects to a specified path or URL.status
must be a valid HTTP status code. If not specified,status
defaults to "307" "Temporary redirect".
Notice: api.js and page.js are conflicting, when both exist, only page.js will be enabled.
Router components
<Link>
This component renders an anchor tag and is the primary way the user will navigate around your website. Anywhere you would have used <a href="...">
you should now use <Link to="..."/>
to get all the performance benefits of client-side routing in Shuvi.
Props
interface LinkProps
extends Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, "href"> {
to: PathRecord;
// route.push or route.replace
replace?: boolean;
state?: State;
// default to true, whether to prefetch resource
prefetch?: boolean;
}
Example
import { Link } from "@shuvi/runtime";
export default function Navigation() {
return (
<nav>
<Link to="/home">Home</Link>
<Link to="/about">about</Link>
</nav>
);
}
<RouterView>
Component to display the current route the user is at.
Example
With the following routes definition:
export const routes = [
{
path: "users",
component: "Users",
children: [
{
path: ":userId",
component: "UserDetail",
},
],
},
];
function App() {
return (
<div>
<h1>App</h1>
<Router>
// this will render Users component
<Outlet />
<Router>
</div>
);
}
function Users() {
return (
<div>
<h1>Users</h1>
// this will render UserDetail component
<Outlet />
</div>
);
}
Router hooks
useLoaderData
This hook returns the JSON parsed data from your route loader function.
Example
import { useLoaderData } from "@shuvi/runtime";
export async function loader() {
return fetchUserDetail();
}
export default function UserProfile() {
const userInfo = useLoaderData();
// ...
}
useParams
The useParams
hook returns an object of key/value pairs of the dynamic params from the current URL that were matched by the route.path
. Child routes inherit all params from their parent routes.
Type
function useParams<T extends {} = {}>(): Readonly<T>;
Example
import * as React from "react";
import { Routes, Route, useParams } from "react-router-dom";
function ProfilePage() {
// Get the userId param from the URL.
let { userId } = useParams();
// ...
}
function App() {
return (
<Router
routes={[
{
path: "users",
children: [
{
path: ":userId",
componnet: ProfilePage,
},
{
path: "me",
},
],
},
]}
></Router>
);
}
useCurrentRoute
Return the matched route with the current URL.
Type
function useCurrentRoute(): import("@shuvi/router").IRoute<{
path: string;
component?: any;
children?: any[] | undefined;
props?: object;
redirect?: string | undefined;
}>;
useRouter
Return router
instance.
Type
interface IRouter<
RouteRecord extends {
path: string;
} = any
> {
push(to: PathRecord, state?: any): void;
replace(to: PathRecord, state?: any): void;
go: History["go"];
back: History["back"];
block: History["block"];
resolve: History["resolve"];
forward(): void;
routes: RouteRecord[];
match(to: PathRecord): Array<IRouteMatch<RouteRecord>>;
listen: (listener: Listener) => RemoveListenerCallback;
beforeEach: (listener: NavigationGuardHook) => RemoveListenerCallback;
beforeResolve: (listener: NavigationGuardHook) => RemoveListenerCallback;
afterEach: (listener: NavigationResolvedHook) => RemoveListenerCallback;
}
function useRouter(): IRouter;