Skip to content

Commit

Permalink
[RFC]: useRoutePaths (#9755)
Browse files Browse the repository at this point in the history
  • Loading branch information
Tobbe authored Dec 27, 2023
1 parent 39cc3aa commit f3e1c37
Show file tree
Hide file tree
Showing 7 changed files with 150 additions and 11 deletions.
41 changes: 41 additions & 0 deletions docs/docs/router.md
Original file line number Diff line number Diff line change
Expand Up @@ -447,6 +447,47 @@ const App = () => {
}
```

## useRoutePaths

`useRoutePaths()` is a React hook you can use to get a map of all routes mapped to their literal paths, as they're defined in your routes file.

Example usage:

```jsx
const routePaths = useRoutePaths()

return <pre><code>{JSON.stringify(routePaths, undefined, 2)}</code></pre>
```

Example output:

```
{
"home": "/"
"about": "/about",
"login": "/login",
"signup": "/signup",
"forgotPassword": "/forgot-password",
"resetPassword": "/reset-password",
"newContact": "/contacts/new",
"editContact": "/contacts/{id:Int}/edit",
"contact": "/contacts/{id:Int}",
"contacts": "/contacts",
}
```

## useRoutePath

This is a convenience hook for when you only want the path for a single route.
```jsx
const aboutPath = useRoutePath('about') // returns "/about"
```
is the same as
```jsx
const routePaths = useRoutePaths()
const aboutPath = routePaths.about // Also returns "/about"
```

## Navigation

### navigate
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ ${routes.map(
}
).join('\n')}
}

export function useRoutePaths(): Record<keyof AvailableRoutes, string>
export function useRoutePath(routeName: keyof AvailableRoutes): string
}

//# sourceMappingURL=web-routerRoutes.d.ts.map
51 changes: 51 additions & 0 deletions packages/router/src/__tests__/useRoutePaths.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/** @jest-environment jsdom */
import React from 'react'

import { render } from '@testing-library/react'

import { Route, Router } from '../router'
import { Set } from '../Set'
import { useRoutePaths, useRoutePath } from '../useRoutePaths'

test('useRoutePaths and useRoutePath', async () => {
const HomePage = () => {
const routePaths = useRoutePaths()
// Sorry about the `as never` stuff here. In an actual project we have
// generated types to use, but not here
const homePath = useRoutePath('home' as never)

return (
<>
<h1>Home Page</h1>
<p>My path is {homePath}</p>
<p>All paths: {Object.values(routePaths).join(',')}</p>
</>
)
}

interface LayoutProps {
children: React.ReactNode
}

const Layout = ({ children }: LayoutProps) => <>{children}</>

const Page = () => <h1>Page</h1>

const TestRouter = () => (
<Router>
<Route path="/" page={HomePage} name="home" />
<Set wrap={Layout}>
<Route path="/one" page={Page} name="one" />
</Set>
<Set wrap={Layout}>
<Route path="/two/{id:Int}" page={Page} name="two" />
</Set>
</Router>
)

const screen = render(<TestRouter />)

await screen.findByText('Home Page')
await screen.findByText(/^My path is\s+\/$/)
await screen.findByText(/^All paths:\s+\/,\/one,\/two\/\{id:Int\}$/)
})
2 changes: 2 additions & 0 deletions packages/router/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ export * from './route-announcement'
export { default as RouteFocus } from './route-focus'
export * from './route-focus'

export * from './useRoutePaths'

export { parseSearch, getRouteRegexAndParams, matchPath } from './util'

/**
Expand Down
6 changes: 5 additions & 1 deletion packages/router/src/router-context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import React, { useReducer, createContext, useContext } from 'react'
import type { AuthContextInterface } from '@redwoodjs/auth'
import { useNoAuth } from '@redwoodjs/auth'

import type { ParamType } from './util'
import type { ParamType, analyzeRoutes } from './util'

type UseAuth = () => AuthContextInterface<
unknown,
Expand All @@ -23,6 +23,7 @@ type UseAuth = () => AuthContextInterface<
export interface RouterState {
paramTypes?: Record<string, ParamType>
useAuth: UseAuth
routes: ReturnType<typeof analyzeRoutes>
}

const RouterStateContext = createContext<RouterState | undefined>(undefined)
Expand All @@ -45,6 +46,7 @@ const RouterSetContext = createContext<
export interface RouterContextProviderProps
extends Omit<RouterState, 'useAuth'> {
useAuth?: UseAuth
routes: ReturnType<typeof analyzeRoutes>
children: React.ReactNode
}

Expand All @@ -55,11 +57,13 @@ function stateReducer(state: RouterState, newState: Partial<RouterState>) {
export const RouterContextProvider: React.FC<RouterContextProviderProps> = ({
useAuth,
paramTypes,
routes,
children,
}) => {
const [state, setState] = useReducer(stateReducer, {
useAuth: useAuth || useNoAuth,
paramTypes,
routes,
})

return (
Expand Down
31 changes: 21 additions & 10 deletions packages/router/src/router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@ function Route(_props: RouteProps | RedirectRouteProps | NotFoundRouteProps) {
return <></>
}

export interface RouterProps extends RouterContextProviderProps {
export interface RouterProps
extends Omit<RouterContextProviderProps, 'routes'> {
trailingSlashes?: TrailingSlashesTypes
pageLoadingDelay?: number
children: ReactNode
Expand Down Expand Up @@ -91,13 +92,7 @@ const LocationAwareRouter: React.FC<RouterProps> = ({
}) => {
const location = useLocation()

const {
pathRouteMap,
hasHomeRoute,
namedRoutesMap,
NotFoundPage,
activeRoutePath,
} = useMemo(() => {
const analyzeRoutesResult = useMemo(() => {
return analyzeRoutes(children, {
currentPathName: location.pathname,
// @TODO We haven't handled this with SSR/Streaming yet.
Expand All @@ -106,6 +101,14 @@ const LocationAwareRouter: React.FC<RouterProps> = ({
})
}, [location.pathname, children, paramTypes])

const {
pathRouteMap,
hasHomeRoute,
namedRoutesMap,
NotFoundPage,
activeRoutePath,
} = analyzeRoutesResult

// Assign namedRoutes so it can be imported like import {routes} from 'rwjs/router'
// Note that the value changes at runtime
Object.assign(namedRoutes, namedRoutesMap)
Expand All @@ -130,7 +133,11 @@ const LocationAwareRouter: React.FC<RouterProps> = ({
if (!activeRoutePath) {
if (NotFoundPage) {
return (
<RouterContextProvider useAuth={useAuth} paramTypes={paramTypes}>
<RouterContextProvider
useAuth={useAuth}
paramTypes={paramTypes}
routes={analyzeRoutesResult}
>
<ParamsProvider>
<PageLoadingContextProvider delay={pageLoadingDelay}>
<ActiveRouteLoader
Expand Down Expand Up @@ -165,7 +172,11 @@ const LocationAwareRouter: React.FC<RouterProps> = ({

// Level 2/3 (LocationAwareRouter)
return (
<RouterContextProvider useAuth={useAuth} paramTypes={paramTypes}>
<RouterContextProvider
useAuth={useAuth}
paramTypes={paramTypes}
routes={analyzeRoutesResult}
>
<ParamsProvider allParams={allParams}>
<PageLoadingContextProvider delay={pageLoadingDelay}>
{redirect && <Redirect to={replaceParams(redirect, allParams)} />}
Expand Down
27 changes: 27 additions & 0 deletions packages/router/src/useRoutePaths.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { useRouterState } from './router-context'
import type { GeneratedRoutesMap } from './util'

import type { AvailableRoutes } from '.'

// This has to be a function, otherwise we're not able to do declaration merging
export function useRoutePaths() {
const routerState = useRouterState()

const routePaths = Object.values(routerState.routes.pathRouteMap).reduce<
Record<keyof GeneratedRoutesMap, string>
>((routePathsAcc, currRoute) => {
if (currRoute.name) {
routePathsAcc[currRoute.name] = currRoute.path
}

return routePathsAcc
}, {})

return routePaths
}

export function useRoutePath(routeName: keyof AvailableRoutes) {
const routePaths = useRoutePaths()

return routePaths[routeName]
}

0 comments on commit f3e1c37

Please sign in to comment.