Skip to content

Commit

Permalink
feat: add object type guard middleware
Browse files Browse the repository at this point in the history
  • Loading branch information
Col0ring committed Jul 21, 2022
1 parent ca5753c commit 0cae0c5
Show file tree
Hide file tree
Showing 8 changed files with 189 additions and 22 deletions.
32 changes: 29 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ import {
} from 'react-router-guarded-routes'

const logGuard: GuardMiddleware = (to, from, next) => {
console.log(to) // { location, matches }
console.log(to) // { location, matches, route }
console.log(from)
next() // call next function to show the route element
// it accepts the same parameters as navigate (useNavigate()) and behaves consistently.
Expand Down Expand Up @@ -138,7 +138,22 @@ const fooGuard: GuardMiddleware = (to, from, next) => {
next()
}

const guards = [logGuard]
// 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]
const fooGuards = [fooGuard]

export default function App() {
Expand Down Expand Up @@ -238,11 +253,22 @@ export interface FromGuardRouteOptions {
matches: GuardedRouteMatch[]
}

export type GuardMiddleware<T = any> = (
export type GuardMiddlewareFunction<T = any> = (
to: ToGuardRouteOptions,
from: FromGuardRouteOptions,
next: NextFunction<T>
) => Promise<void> | void

export type GuardMiddlewareObject<T = any> = {
handler: GuardMiddlewareFunction<T>
register?: (
to: ToGuardRouteOptions,
from: FromGuardRouteOptions
) => Promise<boolean> | boolean
}
export type GuardMiddleware<T = any> =
| GuardMiddlewareFunction<T>
| GuardMiddlewareObject<T>
```
### Components
Expand Down
2 changes: 1 addition & 1 deletion __tests__/guard-config-provider.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ describe('<GuardConfigProvider />', () => {

const button = renderer.root.findByType('button')

TestRenderer.act(() => {
await TestRenderer.act(() => {
button.props.onClick()
})

Expand Down
79 changes: 78 additions & 1 deletion __tests__/guard-provider.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { MemoryRouter, Outlet } from 'react-router'
import { MemoryRouter, Outlet, useNavigate } from 'react-router'
import type { ReactTestRenderer } from 'react-test-renderer'
import TestRenderer from 'react-test-renderer'
import {
Expand Down Expand Up @@ -154,6 +154,83 @@ describe('<GuardProvider />', () => {
`)
})

it('should register a guard middleware when matched a route', async () => {
function Home() {
const navigate = useNavigate()
return (
<div>
home
<button onClick={() => navigate('/about')}>Click</button>
</div>
)
}
let renderer!: ReactTestRenderer

await TestRenderer.act(async () => {
const routes: GuardedRouteObject[] = [
{
element: (
<GuardProvider
guards={[
{
handler: () => {},
register(to) {
if (to.location.pathname === '/about') {
return true
}
return false
},
},
]}
fallback={<div>loading...</div>}
>
<Outlet />
</GuardProvider>
),
children: [
{
path: 'home',
element: <Home />,
},
{
path: 'about',
element: <h1>about</h1>,
},
],
},
]

await TestRenderer.act(() => {
renderer = TestRenderer.create(
<MemoryRouter initialEntries={['/home']}>
<RoutesRenderer routes={routes} />
</MemoryRouter>
)
})
expect(renderer.toJSON()).toMatchInlineSnapshot(`
<div>
home
<button
onClick={[Function]}
>
Click
</button>
</div>
`)
})
await TestRenderer.act(async () => {
const button = renderer.root.findByType('button')
await TestRenderer.act(() => {
button.props.onClick()
})
expect(renderer.toJSON()).toMatchInlineSnapshot(`
<div>
loading...
</div>
`)
})
})

it('should include all guard middleware when there are nested providers', async () => {
await TestRenderer.act(async () => {
const routes: GuardedRouteObject[] = [
Expand Down
31 changes: 31 additions & 0 deletions __tests__/useGuardedRoutes.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,37 @@ describe('useGuardedRoutes', () => {
</div>
`)
})

it('pass an object as a middleware and provide a register handler', async () => {
await TestRenderer.act(async () => {
const routes: GuardedRouteObject[] = [
{
path: 'home',
element: <h1>home</h1>,
guards: [
{
handler: () => {},
register: () => false,
},
],
},
]

let renderer!: ReactTestRenderer
await TestRenderer.act(() => {
renderer = TestRenderer.create(
<MemoryRouter initialEntries={['/home']}>
<RoutesRenderer routes={routes} />
</MemoryRouter>
)
})
expect(renderer.toJSON()).toMatchInlineSnapshot(`
<h1>
home
</h1>
`)
})
})
})

describe('when a guard has called the `next()` function', () => {
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "react-router-guarded-routes",
"version": "0.1.2",
"version": "0.2.0",
"description": "a guard middleware for react-router v6",
"keywords": [
"react",
Expand Down
40 changes: 27 additions & 13 deletions src/internal/guard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,14 @@ import {
import {
FromGuardRouteOptions,
GuardedRouteObject,
GuardMiddleware,
GuardMiddlewareFunction,
NextFunction,
ToGuardRouteOptions,
} from '../type'
import { useGuardConfigContext } from './useGuardConfigContext'
import { useGuardContext } from './useGuardContext'
import { usePrevious } from './usePrevious'
import { isNumber, isPromise } from './utils'
import { isFunction, isNumber, isPromise } from './utils'

export interface GuardProps {
route: GuardedRouteObject
Expand Down Expand Up @@ -71,13 +71,15 @@ export const Guard: React.FC<GuardProps> = (props) => {
() => ({
location: location.to as Location,
matches,
route: matches[matches.length - 1]?.route,
}),
[location.to, matches]
)
const fromGuardRouteOptions: FromGuardRouteOptions = useMemo(
() => ({
location: location.from,
matches: prevMatches || [],
route: prevMatches ? prevMatches[prevMatches.length - 1]?.route : null,
}),
[location.from, prevMatches]
)
Expand All @@ -92,7 +94,7 @@ export const Guard: React.FC<GuardProps> = (props) => {
)

const runGuard = useCallback(
(guard: GuardMiddleware, prevCtxValue: any) => {
(guard: GuardMiddlewareFunction, prevCtxValue: any) => {
return new Promise<GuardedResult<any>>((resolve, reject) => {
let ctxValue: any
let called = false
Expand Down Expand Up @@ -140,15 +142,11 @@ export const Guard: React.FC<GuardProps> = (props) => {
ctxValue = value
return next()
}
async function handleGuard() {
await guard(toGuardRouteOptions, fromGuardRouteOptions, next)
}
try {
const guardResult = guard(
toGuardRouteOptions,
fromGuardRouteOptions,
next
)
if (isPromise(guardResult)) {
guardResult.catch((error) => reject(error))
}
handleGuard()
} catch (error) {
reject(error)
}
Expand All @@ -160,7 +158,23 @@ export const Guard: React.FC<GuardProps> = (props) => {
const runGuards = useCallback(async () => {
let ctxValue: any
for (const guard of guards) {
const result = await runGuard(guard, ctxValue)
let registered = true
let guardHandle: GuardMiddlewareFunction
if (isFunction(guard)) {
guardHandle = guard
} else {
guardHandle = guard.handler
if (guard.register) {
registered = await guard.register(
toGuardRouteOptions,
fromGuardRouteOptions
)
}
}
if (!registered) {
continue
}
const result = await runGuard(guardHandle, ctxValue)
ctxValue = result.value
if (result.type === ResolvedStatus.NEXT) {
continue
Expand All @@ -173,7 +187,7 @@ export const Guard: React.FC<GuardProps> = (props) => {
}
}
setValidated(true)
}, [guards, navigate, runGuard])
}, [fromGuardRouteOptions, guards, navigate, runGuard, toGuardRouteOptions])

useEffect(() => {
function validate() {
Expand Down
4 changes: 4 additions & 0 deletions src/internal/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,7 @@ export function isPromise<T = any>(value: any): value is Promise<T> {
export function isNumber(value: any): value is number {
return typeof value === 'number'
}

export function isFunction(value: any): value is (...args: any[]) => any {
return typeof value === 'function'
}
21 changes: 18 additions & 3 deletions src/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,17 +30,32 @@ export interface GuardedRouteMatch<ParamKey extends string = string>
export interface ToGuardRouteOptions {
location: Location
matches: GuardedRouteMatch[]
route: GuardedRouteObject
}

export interface FromGuardRouteOptions
extends ReplacePick<
ToGuardRouteOptions,
['location'],
[ToGuardRouteOptions['location'] | null]
['location', 'route'],
[
ToGuardRouteOptions['location'] | null,
ToGuardRouteOptions['route'] | null
]
> {}

export type GuardMiddleware<T = any> = (
export type GuardMiddlewareFunction<T = any> = (
to: ToGuardRouteOptions,
from: FromGuardRouteOptions,
next: NextFunction<T>
) => Promise<void> | void

export type GuardMiddlewareObject<T = any> = {
handler: GuardMiddlewareFunction<T>
register?: (
to: ToGuardRouteOptions,
from: FromGuardRouteOptions
) => Promise<boolean> | boolean
}
export type GuardMiddleware<T = any> =
| GuardMiddlewareFunction<T>
| GuardMiddlewareObject<T>

0 comments on commit 0cae0c5

Please sign in to comment.