English | 简体ä¸ć–‡
A guard middleware for react-router v6, inspired by react-router-guards
.
npm install react-router-guarded-routes react-router --save
# or
yarn add react-router-guarded-routes react-router
# or
pnpm add react-router-guarded-routes react-router
Provides GuardConfigProvider
in BrowserRouter
, and you can use it like react-router
(compatible with the apis of react-router
).
import { BrowserRouter } from 'react-router-dom'
import {
GuardConfigProvider,
GuardedRoute,
GuardedRoutes,
} from 'react-router-guarded-routes'
export default function App() {
return (
<BrowserRouter>
<GuardConfigProvider>
<GuardedRoutes>
<GuardedRoute element={<div>foo</div>} path="/foo" />
<GuardedRoute element={<div>bar</div>} path="/bar/*">
<GuardedRoute element={<div>baz</div>} path="/bar/baz" />
</GuardedRoute>
</GuardedRoutes>
</GuardConfigProvider>
</BrowserRouter>
)
}
Use hooks:
import {
GuardedRouteObject,
useGuardedRoutes,
} from 'react-router-guarded-routes'
const routes: GuardedRouteObject[] = [
{ path: '/foo', element: <div>foo</div> },
{
path: '/bar/*',
element: <div>bar</div>,
children: [{ path: '/bar/baz', element: <div>baz</div> }],
},
]
function Routes() {
return <GuardedRoutes>{useGuardedRoutes([routes])}</GuardedRoutes>
}
export default function App() {
return (
<BrowserRouter>
<GuardConfigProvider>
<Routes />
</GuardConfigProvider>
</BrowserRouter>
)
}
You can provide GuardProvider
with multiple guards middleware for route guarding, GuardProvider
can receive an array of guards and a fallback element (can be used to load loading state).
import { BrowserRouter } from 'react-router-dom'
import {
GuardConfigProvider,
GuardedRoute,
GuardedRoutes,
GuardMiddleware,
GuardProvider,
} from 'react-router-guarded-routes'
const logGuard: GuardMiddleware = (to, from, next) => {
console.log(to) // { location, matches, route }
console.log(from)
next() // call next function to run the next middleware or show the route element, it accepts the same parameters as navigate (useNavigate()) and behaves consistently.
}
// you can use object to determine whether you need to register middleware
const barGuard: GuardMiddleware = {
handler: (to, from, next) => {
console.log('bar')
next()
},
register: (to, from) => {
// only matched with `/bar` can be executed.
if (to.location.pathname.startsWith('/bar')) {
return true
}
return false
},
}
const guards = [logGuard, barGuard]
export default function App() {
return (
<BrowserRouter>
<GuardConfigProvider>
{/* Guard all routes below. */}
<GuardProvider fallback={<div>loading...</div>} guards={guards}>
<GuardedRoutes>
<GuardedRoute element={<div>foo</div>} path="/foo" />
<GuardedRoute element={<div>bar</div>} path="/bar/*">
<GuardedRoute element={<div>baz</div>} path="/bar/baz" />
</GuardedRoute>
</GuardedRoutes>
</GuardProvider>
</GuardConfigProvider>
</BrowserRouter>
)
}
Of course, you can also set up separate fallbacks and guards for each route.
import { BrowserRouter, Outlet } from 'react-router-dom'
import {
GuardConfigProvider,
GuardedRoute,
GuardedRoutes,
GuardMiddleware,
GuardProvider,
} from 'react-router-guarded-routes'
const logGuard: GuardMiddleware = (to, from, next) => {
console.log(to, from)
next()
}
const fooGuard: GuardMiddleware = (to, from, next) => {
console.log('foo')
next()
}
const guards = [logGuard]
const fooGuards = [fooGuard]
export default function App() {
return (
<BrowserRouter>
<GuardConfigProvider>
<GuardProvider fallback={<div>loading...</div>} guards={guards}>
<GuardedRoutes>
<GuardedRoute
fallback={<div>loading foo...</div>}
guards={fooGuard}
element={<div>foo</div>}
path="/foo"
/>
<GuardedRoute
element={
<div>
bar
<Outlet />
</div>
}
path="/bar/*"
>
<GuardedRoute element={<div>baz</div>} path="/bar/baz" />
</GuardedRoute>
</GuardedRoutes>
</GuardProvider>
</GuardConfigProvider>
</BrowserRouter>
)
}
You can also call next.ctx('ctx value')
to transfer contextual information, and get it by ctxValue
in the next guard middleware. The guard middleware is executed from outside to inside, left to right.
<GuardConfigProvider>
<GuardProvider
fallback={<div>loading...</div>}
guards={(to, from, next) => {
next.ctx('ctx value')
}}
>
<GuardedRoutes>
<GuardedRoute
guards={(to, from, next, { ctxValue }) => {
console.log(ctxValue) // ctx value
next()
}}
element={<div>foo</div>}
path="/foo"
/>
</GuardedRoutes>
</GuardProvider>
</GuardConfigProvider>
And call next.end()
to ignore remaining middleware.
<GuardConfigProvider>
<GuardProvider
fallback={<div>loading...</div>}
guards={
((to, from, next) => {
next.end()
},
() => {
console.log('will not be called')
})
}
>
<GuardedRoutes>
<GuardedRoute
guards={() => {
console.log('will not be called')
}}
element={<div>foo</div>}
path="/foo"
/>
</GuardedRoutes>
</GuardProvider>
</GuardConfigProvider>
import React from 'react'
import {
Location,
NavigateFunction,
RouteMatch,
RouteObject,
} from 'react-router'
import { ReplacePick } from 'types-kit'
export interface GuardedRouteConfig {
guards?: GuardMiddleware[]
fallback?: React.ReactNode
[props: PropertyKey]: any
}
export type GuardedRouteObject = RouteObject &
GuardedRouteConfig & {
children?: GuardedRouteObject[]
}
export interface NextFunction<T> extends NavigateFunction {
(): void
ctx: (value: T) => void
end: () => void
}
export interface GuardedRouteMatch<ParamKey extends string = string>
extends Omit<RouteMatch<ParamKey>, 'route'> {
route: GuardedRouteObject
}
export interface ToGuardRouteOptions {
location: Location
matches: GuardedRouteMatch[]
route: GuardedRouteObject
}
export interface FromGuardRouteOptions
extends ReplacePick<
ToGuardRouteOptions,
['location', 'route'],
[
ToGuardRouteOptions['location'] | null,
ToGuardRouteOptions['route'] | null
]
> {}
export interface ExternalOptions<T, I> {
ctxValue: T
injectedValue: I
}
export type GuardMiddlewareFunction<T = any, I = any> = (
to: ToGuardRouteOptions,
from: FromGuardRouteOptions,
next: NextFunction<T>,
externalOptions: ExternalOptions<T, I>
) => Promise<void> | void
export type GuardMiddlewareObject<T = any, I = any> = {
handler: GuardMiddlewareFunction<T, I>
register?: (
to: ToGuardRouteOptions,
from: FromGuardRouteOptions
) => Promise<boolean> | boolean
}
export type GuardMiddleware<T = any, I = any> =
| GuardMiddlewareFunction<T, I>
| GuardMiddlewareObject<T, I>
The GuardConfigProvider
has configuration about routing, should not be used more than one in an app, make sure it's at the topmost level inside the Router (BrowserRouter
and HashRouter
).
And it provides APIs for whether to run guard middleware and whether to display the fallback element:
import React from 'react'
export interface GuardConfigProviderProps {
enableGuard?: (
location: ToGuardRouteOptions,
prevLocation: FromGuardRouteOptions
) => Promise<boolean> | boolean
enableFallback?: (
location: ToGuardRouteOptions,
prevLocation: FromGuardRouteOptions
) => boolean
children: React.ReactNode
}
Prop | Optional | Default | Description |
---|---|---|---|
enableGuards |
Yes | (to, from) => to.location.pathname !== from.location?.pathname | whether to run guard middleware |
enableFallback |
Yes | () => true | whether to display the fallback element |
import { BrowserRouter } from 'react-router-dom'
import { GuardConfigProvider } from 'react-router-guarded-routes'
export default function App() {
return (
<BrowserRouter>
<GuardConfigProvider>
{
// routes
}
</GuardConfigProvider>
</BrowserRouter>
)
}
It provides public fallback element and guard middleware for GuardedRoute
.
import React from 'react'
export interface GuardProviderProps {
fallback?: React.ReactElement
useInject?: (
to: ToGuardRouteOptions,
from: FromGuardRouteOptions
) => Record<string, any>
guards?: GuardedRouteConfig['guards']
children: React.ReactNode
}
Prop | Optional | Default | Description |
---|---|---|---|
fallback |
Yes | a fallback element to show when a GuardedRoute run guard middleware |
|
useInject |
Yes | an injected value (React hooks can be used) for guard middleware to use, will be automatically merged the values of nested GuardProvider |
|
guards |
Yes | the guards to set for routes inside the GuardProvider |
import { BrowserRouter } from 'react-router-dom'
import {
GuardConfigProvider,
GuardedRoute,
GuardedRoutes,
GuardMiddleware,
GuardProvider,
} from 'react-router-guarded-routes'
const logGuard: GuardMiddleware = (to, from, next) => {
console.log(to, from)
next()
}
export default function App() {
return (
<BrowserRouter>
<GuardConfigProvider>
<GuardProvider fallback={<div>loading...</div>} guards={[logGuard]}>
<GuardedRoutes>
<GuardedRoute element={<div>foo</div>} path="/foo" />
</GuardedRoutes>
</GuardProvider>
</GuardConfigProvider>
</BrowserRouter>
)
}
Use nested GuardProvider:
<GuardConfigProvider>
<GuardProvider fallback={<div>loading...</div>}>
<GuardedRoutes>
<GuardedRoute element={<div>foo</div>} path="/foo" />
<GuardProvider fallback={<div>loading2...</div>}>
<GuardedRoute
element={
<div>
bar
<Outlet />
</div>
}
path="/bar/*"
>
<GuardedRoute element={<div>baz</div>} path="/bar/baz" />
</GuardedRoute>
</GuardProvider>
</GuardedRoutes>
</GuardProvider>
</GuardConfigProvider>
Inject value:
import { createContext } from 'react'
import { BrowserRouter } from 'react-router-dom'
import {
GuardConfigProvider,
GuardedRoute,
GuardedRoutes,
GuardProvider,
} from 'react-router-guarded-routes'
export const AuthContext = createContext({
isLogin: false,
})
export function useAuth() {
return useContext(AuthContext)
}
export default function App() {
return (
<BrowserRouter>
<AuthContext>
<GuardConfigProvider>
<GuardProvider
fallback={<div>loading...</div>}
useInject={useAuth}
guards={[
(to, from, next, { injectedValue }) => {
console.log(injectedValue) // { isLogin: false }
next()
},
]}
>
<GuardedRoutes>
<GuardedRoute element={<div>foo</div>} path="/foo" />
</GuardedRoutes>
</GuardProvider>
</GuardConfigProvider>
</AuthContext>
</BrowserRouter>
)
}
The GuardedRoutes
component acts as a replacement for the default Routes
component provided by React Router.
import { RoutesProps } from 'react-router'
export interface GuardedRoutesProps extends RoutesProps {}
<BrowserRouter>
<GuardConfigProvider>
<GuardedRoutes>
<GuardedRoute element={<div>foo</div>} path="/foo" />
</GuardedRoutes>
</GuardConfigProvider>
</BrowserRouter>
The GuardedRoute
component acts as a replacement for the default Route
component provided by React Router, allowing for routes to use guard middleware and accepting the same props as regular Route
.
import { Route } from 'react-router'
type RouteProps = Parameters<typeof Route>[0]
export type GuardedRouteProps = RouteProps & GuardedRouteConfig
The following table explains the guard-specific props for this component.
Prop | Optional | Default | Description |
---|---|---|---|
fallback |
Yes | a fallback element to show when a GuardedRoute run guard middleware. (it will override the fallback provided by GuardProvider ) |
|
guards |
Yes | the guards to set for the route |
<GuardedRoutes>
<GuardedRoute
element={<div>foo</div>}
path="/foo"
fallback={<div>loading...</div>}
guards={[
(to, from, next) => {
next()
},
]}
/>
</GuardedRoutes>
The useGuardedRoutes
hook acts as a replacement for the default useRoutes
hook provided by React Router, and additionally provides fallback
and guards
properties for each member.
import { useRoutes } from 'react-router'
type LocationArg = Parameters<typeof useRoutes>[1]
export function useGuardedRoutes(
guardedRoutes: GuardedRouteObject[],
locationArg?: LocationArg
): ReturnType<typeof useRoutes>
import {
GuardedRouteObject,
useGuardedRoutes,
} from 'react-router-guarded-routes'
const routes: GuardedRouteObject[] = [
{
path: '/foo',
element: <div>foo</div>,
fallback: <div>loading foo...</div>,
guards: [(to, from, next) => next()],
},
]
function Routes() {
return <>{useGuardedRoutes(routes)}</>
}
export default function App() {
return (
<BrowserRouter>
<GuardConfigProvider>
<GuardProvider fallback={<div>loading...</div>}>
<Routes>
</GuardProvider>
</GuardConfigProvider>
</BrowserRouter>
)
}