Modern.js build-in provides partial support for React Router 6 and provides various types of routing modes. According to different entry types, routing is divided into three modes, namely Conventional routing, Self-controlled routing and Other.
NOTE
The routes mentioned in this section are client routes, that is, SPA routes.
With routes/
as the agreed entry, Modern.js will automatically generate the corresponding routing structure based on the file system.
Modern.js supports the popular convention routing mode in the industry: nested routing. When using nested routing, the routing of the page corresponds the UI structure, and we will introduce this routing mode in detail.
/user/johnny/profile /user/johnny/posts
+------------------+ +-----------------+
| User | | User |
| +--------------+ | | +-------------+ |
| | Profile | | +------------> | | Posts | |
| | | | | | | |
| +--------------+ | | +-------------+ |
+------------------+ +-----------------+
There are two file conventions in the routes/
directory layout.[jt]sx
and page.[jt]sx
(abbreviated as .tsx
later). These two files determine the layout hierarchy of the application, where layout.tsx
is used as the layout component, and page.tsx
is used as the content component, which is the leaf node of the entire routing table.
For example, here routes/layout.tsx
will be used as the layout component of all components under the /
route:
.
└── routes
├── layout.tsx
├── page.tsx
└── user
├── layout.tsx
└── page.tsx
When the route is /
, there will be the following UI layout:
<Layout>
<Page />
</Layout>
Similarly, routes/user/layout.tsx
will be used as a layout component for all components under the /user
route. When the route is /user
, the following UI layout will be available:
<Layout>
<UserLayout>
<UserPage>
<UserLayout>
</Layout>
<Layout>
component refers to all layout.tsx
files in the routes/
directory, which represent the layout of the corresponding route segment, and use <Outlet>
to represent sub-components.
NOTE
<Outlet>
is a new API in React Router 6, see Outlet for details.
In order to facilitate the introduction of the relationship between <Layout>
and <Outlet>
, the following file directory example:
.
└── routes
├── blog
│ └── page.tsx
├── layout.tsx
├── page.tsx
└── user
├── layout.tsx
└── page.tsx
/
, <Outlet>
in routes/layout.tsx
represents the component exported in routes/page.tsx
, generating the following UI structure:<Layout>
<Page />
</Layout>
/blog
, <Outlet>
in routes/layout.tsx
represents the component exported in routes/blog/page.tsx
, generating the following UI structure:<Layout>
<BlogPage />
</Layout>
/user
, <Outlet>
in routes/layout.tsx
represents the component exported in routes/user/layout.tsx
. <Outlet>
in routes/user/layout.tsx
represents the component exported in routes/user/page.tsx
. Generate the following UI structure:<Layout>
<UserLayout>
<UserPage>
<UserLayout>
</Layout>
In summary, if there is a layout.tsx
in the file directory of the subroute, the <Outlet>
in the previous layout.tsx
is the layout.tsx
under the file directory of the subroute, otherwise it is the page.tsx
under the file directory of the subroute.
All routes should end with the <Page>
component. In the page.tsx
file, if the developer introduces the <Outlet>
component, it will have no effect.
With a file directory named []
, the generated route will be used as a dynamic route. For example the following file directory:
└── routes
├── [id]
│ └── page.tsx
├── blog
│ └── page.tsx
└── page.tsx
The routes/[id]/page.tsx
file is converted to the /:id
route. Except for the /blog
route that exactly matches, all other /xxx
will match this route.
In component, you can get the corresponding named parameters through useParams.
If a $.tsx
file is created in the routes directory, this file will act as a wildcard route component that will be rendered when there is no matching route.
NOTE
$.tsx
can be thought of as a special page
routing component that renders $.tsx
as a child of layout
when there is a layout
component in the current directory.
For example, the following directory structure:
└── routes
├── $.tsx
├── blog
│ └── page.tsx
└── page.tsx
The routes/$.tsx
component is rendered when accessing any path that does not match, and again, the remainder of the url can be captured in $.tsx
using useParams.
import { useParams } from '@modern-js/runtime/router';
// When the path is `/aaa/bbb`
const params = useParams();
params['*'] // => 'aaa/bbb'
When a directory name begins with __
, the corresponding directory name is not converted to the actual routing path, such as the following file directory:
.
└── routes
├── __auth
│ ├── layout.tsx
│ ├── login
│ │ └── page.tsx
│ └── signup
│ └── page.tsx
├── layout.tsx
└── page.tsx
Modern.js will generate two routes, /login
and /sign
, __auth/layout.tsx
component will be used as the layout component of login/page.tsx
and signup/page.tsx
, but __auth
will not be used as the route path fragment.
This feature is useful when you need to do separate layouts for certain types of routes, or when you want to categorize routes.
In some cases, the project needs more sophisticated routes, but these routes do not have independent UI layouts. If you create a route like a normal file directory, the directory level will be deeper.
Therefore Modern.js supports splitting routing fragments by .
instead of file directory. For example, when you need /user/profile/2022/edit
, you can directly create the following file:
└── routes
├── user.profile.[id].edit
│ └── page.tsx
├── layout.tsx
└── page.tsx
When accessing a route, you will get the following UI layout:
<RootLayout>
<UserProfileEdit /> // routes/user.profile.[id].edit/page.tsx
</RootLayout>
In each layer directory under routes/
, developers can create loading.tsx
files and export a <Loading>
component by default.
When the component exists in the routing directory, all routing switches under this level of subrouting will use the <Loading>
component as the Fallback UI when JS Chunk is loaded. When the layout.tsx
file is not defined in this directory, the <Loading>
component will not take effect. For example, the following file directory:
.
└── routes
├── blog
│ ├── [id]
│ │ └── page.tsx
│ └── page.tsx
├── layout.tsx
├── loading.tsx
└── page.tsx
When a route jumps from /
to /blog
, if the JS Chunk of the <Blog>
component is not loaded, the component UI exported in loading.tsx
will be displayed first. But when jumping from /blog
to /blog/20220101
, it will not be displayed.
You can redirect routes by creating a data loader
file, Suppose you have the file routes/user/page.tsx
and you want to redirect the route corresponding to this file, you can create the file routes/user/page.loader.ts
:
import { redirect } from "@edenx/runtime/router"
export default () => {
const user = await getUser();
if(!user){
return redirect('/login');
}
return null;
}
In each layer directory under routes/
, the developer can also define a error.tsx
file, and export a <ErrorBoundary>
component by default.
When the component exists in a routing directory, the component render error is caught by the ErrorBoundary
component. The <ErrorBoundary>
component does not take effect when the directory does not have a layout.tsx
file defined.
<ErrorBoundary>
can return the UI view when the error occurred. When the <ErrorBoundary>
component is not declared at the current level, the error will bubble up to the higher component until it is caught or throws an error. At the same time, when a component fails, it will only affect the routed component and sub-component that caught the error. The state and view of other components are not affected and can continue to interact.
Within the <ErrorBoundary>
component, you can use useRouteError to get the specific information of the error:
import { useRouteError } from '@modern-js/runtime/router';
export default const ErrorBoundary = () => {
const error = useRouteError();
return (
<div>
<h1>{error.status}</h1>
<h2>{error.message}</h2>
</div>
)
}
In some scenarios where you need to do some operations before the application renders, you can define init
hooks in routes/layout.tsx
. init
will be executed on both the client and server side, the basic usage example is as follows:
import type { RuntimeContext } from '@modern-js/runtime';
export const init = (context: RuntimeContext) => {
// do something
};
The init
hook allows you to mount some global data and access the runtimeContext
variable from elsewhere in the application:
NOTE
This feature is useful when the application requires pre-page data, custom data injection or framework migration (e.g. Next.js)
import {
RuntimeContext,
} from '@modern-js/runtime';
export const init = (context: RuntimeContext) => {
return {
message: 'Hello World',
}
}
import { useRuntimeContext } from '@modern-js/runtime';
export default () => {
const { context } = useRuntimeContext();
const { message } = context.getInitData();
return <div>{message}</div>;
}
When working with SSR, the browser side can get the data returned by init
during SSR, and the developer can decide whether to retrieve the data on the browser side to overwrite the SSR data, for example:
import { RuntimeContext } from '@modern-js/runtime';
export const init = (context: RuntimeContext) => {
if (process.env.MODERN_TARGET === 'node') {
return {
message: 'Hello World By Server',
}
} else {
const { context } = runtimeContext;
const data = context.getInitData();
// If do not get the expected data
if (!data.message) {
return {
message: 'Hello World By Client'
}
}
}
}
In each root Layout
component (routes/layout.ts
), the application runtime configuration can be dynamically defined:
import type { AppConfig } from '@modern-js/runtime';
export const config = (): AppConfig => {
return {
router: {
supportHtml5History: false
}
}
};
With src/App.tsx
as the agreed entry, Modern.js will not do additional operations with multiple routes, developers can use the React Router 6 API for development by themselves, for example:
import { BrowserRouter, Route, Routes } from '@modern-js/runtime/router';
export default () => {
return (
<BrowserRouter>
<Routes>
<Route index element={<div>index</div>} />
<Route path="about" element={<div>about</div>} />
</Routes>
</BrowserRouter>
);
};
NOTE
Modern.js has a series of resource loading and rendering optimizations to the default convention-based routing, and provides out-of-the-box SSR capabilities, when using self-directed routing, need to be packaged by the developer, and it is recommended that developers use convention-based routing.
By default, Modern.js turn on the built-in routing scheme, React Router.
export default defineConfig({
runtime: {
router: true,
},
});
Modern.js exposes the React Router API from the @modern-js/runtime/router
namespace for developers to use, ensuring that developers and Modern.js use the same code. In addition, in this case, the React Router code will be packaged into JS products. If the project already has its own routing scheme, or does not need to use client routing, this feature can be turned off.
export default defineConfig({
runtime: {
router: false,
},
});