Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [5.0.0] - UNRELEASED
### Added
- Added support for `useRoutes` to take an array to specify route matching priority
### Changed
- Added support for underscore `_` in path part matchers

Expand Down
32 changes: 31 additions & 1 deletion docs/api/useRoutes.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ This hook is the main entry point for an application using raviger. Returns the

```typescript
function useRoutes(
routes: { [key: string]: (props: { [k: string]: any }) => JSX.Element },
routes: { [key: string]: (props: { [k: string]: any }) => JSX.Element }
| { path: string, fn: (props: { [k: string]: any }) => JSX.Element },
options?: {
basePath?: string
routeProps?: { [k: string]: any }
Expand Down Expand Up @@ -126,3 +127,32 @@ export default function App() {
)
}
```

## Using an Array to Control Priority

Raviger will normally match routes in the order they are defined in the routes object, allowing you to control matching priority. However, this behavior is not guaranteed by JS, and if you dynamically construct routes you may have difficulty ordering object keys.

For this case, raviger supports taking an array of `{ path, fn }` objects, where the priority is determined by position in the array.

Consider this case:

```javascript
{
'/comp1/*': () => <h1>Just Comp-1</h1>,
'/comp1/view2/*': () => <h1>Comp-1-view-2</h1>,
'/comp1/view1/*': () => <h1>Comp-1-view1</h1>,
'/comp2/*': () => <h1>Comp-2</h1>
}
```

If the app tries to route to /comp1/view1, instead of matching route /comp1/view1/* it matches /comp1/*. This can be fixed if we define the routes like this

```javascript
[

{ path: '/comp1/view2/*', fn:() => <h1>Comp-1-view-2</h1>, },
{ path: '/comp1/view1/*', fn:() => <h1>Comp-1-view1</h1>, },
{ path: '/comp1/*', fn:() => <h1>Just Comp-1</h1>, },
{ path: '/comp2/*', fn:() => <h1>Comp-2</h1> },
]
```
4 changes: 2 additions & 2 deletions docs/docker-compose.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
version: '2'
version: '3.7'
services:
jekyll:
environment:
Expand All @@ -8,4 +8,4 @@ services:
volumes:
- .:/srv/jekyll
ports:
- 4000:4000
- 4000:4000
2 changes: 1 addition & 1 deletion scripts/start-docs
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
#!/usr/bin/env bash

cd docs && docker-compose up
cd docs && docker compose up
37 changes: 30 additions & 7 deletions src/router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,21 @@ type ExtractPathParams<Path extends string, Parts = Split<Path, '/'>> = Parts ex
: ExtractPathParams<Path, Tail>
: unknown

export type Routes<Path extends string> = {
[P in Path]: (
params: NonEmptyRecord<ExtractPathParams<P extends `${infer P1}*` ? P1 : P>>,
export type Route<Path extends string> = {
path: Path
fn: (
params: NonEmptyRecord<ExtractPathParams<Path extends `${infer P1}*` ? P1 : Path>>,
) => JSX.Element
}

export type Routes<Path extends string> =
| {
[P in Path]: (
params: NonEmptyRecord<ExtractPathParams<P extends `${infer P1}*` ? P1 : P>>,
) => JSX.Element
}
| Route<Path>[]

export function useRoutes<Path extends string>(
routes: Routes<Path>,
{
Expand Down Expand Up @@ -77,7 +86,7 @@ export function useRoutes<Path extends string>(

function useMatchRoute(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
routes: { [key: string]: (...props: any) => JSX.Element },
routes: { [key: string]: (...props: any) => JSX.Element } | Route<string>[],
path: string | null,
{
routeProps,
Expand All @@ -86,14 +95,28 @@ function useMatchRoute(
}: Omit<RouteOptionParams, 'basePath' | 'matchTrailingSlash'> & { matchTrailingSlash: boolean },
) {
path = trailingMatch(path, matchTrailingSlash)
const matchers = useMatchers(Object.keys(routes))
const mappedRoutes = Array.isArray(routes)
? routes
: Object.entries(routes).reduce((arr, [path, fn]) => {
// console.log('route fn', fn())
arr.push({ path, fn })
return arr
}, [] as Route<string>[])
const matchers = useMatchers(mappedRoutes.map((r) => r.path))

// console.log('mapped Routes', mappedRoutes)
// console.log('matchers', matchers)

if (path === null) return null
const [routeMatch, props] = getMatchParams(path, matchers)
const [pathMatch, props] = getMatchParams(path, matchers)

if (!pathMatch) return null

const routeMatch = mappedRoutes.find((r) => r.path == pathMatch.path)

if (!routeMatch) return null

return routes[routeMatch.path](
return routeMatch.fn(
overridePathParams ? { ...props, ...routeProps } : { ...routeProps, ...props },
)
}
Expand Down
31 changes: 31 additions & 0 deletions test/router.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,37 @@ describe('useRoutes', () => {
expect(getByTestId('label')).toHaveTextContent('new route')
})

describe('with array routes', () => {
test('matches catch-all first', async () => {
const routes = [
{ path: '/layer*', fn: () => <Route label="top" /> },
{ path: '/layer/under', fn: () => <Route label="bottom" /> },
]
act(() => navigate('/layer'))
const { getByTestId } = render(<Harness routes={routes} />)

// act(() => navigate('/foo'))
expect(getByTestId('label')).toHaveTextContent('top')
act(() => navigate('/layer/under'))
expect(getByTestId('label')).toHaveTextContent('top')
})

test('matches catch-all last', async () => {
const routes = [
{ path: '/layer/under', fn: () => <Route label="bottom" /> },
{ path: '/layer*', fn: () => <Route label="top" /> },
]

act(() => navigate('/layer'))
const { getByTestId } = render(<Harness routes={routes} />)

// act(() => navigate('/foo'))
expect(getByTestId('label')).toHaveTextContent('top')
act(() => navigate('/layer/under'))
expect(getByTestId('label')).toHaveTextContent('bottom')
})
})

describe('with basePath', () => {
const routes = {
'/': () => <Route label="home" />,
Expand Down
Loading