Skip to content

Commit

Permalink
feat(router): Allow redirect routes to use route names as target (#10376
Browse files Browse the repository at this point in the history
)
  • Loading branch information
Tobbe authored and Josh-Walker-GM committed Apr 2, 2024
1 parent f7227b5 commit d2e5dd4
Show file tree
Hide file tree
Showing 4 changed files with 114 additions and 15 deletions.
34 changes: 34 additions & 0 deletions .changesets/10376.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
- feat(router): Allow redirect routes to use route names as target (#10376) by @Tobbe

When specifying a redirect route like
`<Route path="/simple" redirect="/newSimple" name="simple" />` the value of
`redirect` is the path of the page the user should be redirected to. However,
the paths can be long and annoying to type out. And if they ever change the
redirect would now be broken.

Also, for private routes we do this:
```jsx
<Router>
<Route path="/" page={HomePage} name="home" />
<PrivateSet unauthenticated="home">
<Route path="/admin" page={AdminPage} name="admin" />
</PrivateSet>
</Router>
```
Here, if a user isn't authenticated, the user will be redirected to the `home`
route. Notice how the target route is specified by its name (`home`) instead
of its path (`/`).

With this PR it's now also possible to redirect using the name of the target
route, making our route behavior more consistent. So this will now work

```jsx
<Router>
<Route path="/" page={HomePage} name="home" />
<Route path="/no-longer-exists" redirect="home" />
</Router>
```

Old style paths still works. The logic is super simple -> if the `redirect`
value starts with `/` it's assumed to be a path. If not, it's assumed to be a
route name. This should make this change fully backwards compatible.
13 changes: 11 additions & 2 deletions docs/docs/router.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,11 @@ Some pages should only be visible to authenticated users. We support this using

If you move a page you might still want to keep the old route around, so that
old links to your site keep working. To this end RedwoodJS supports the
`redirect` prop on routes:
`redirect` prop on routes, which allows you to specify the name of the route
you want to redirect to:

```jsx title="Routes.jsx"
<Route path="/blog/{id}" redirect="/post" />
<Route path="/blog/{id}" redirect="post" />
<Route path="/posts/{id}" page="PostPage" name="post" />
```

Expand All @@ -67,6 +68,14 @@ update them all you can remove the name prop and you'll get TypeScript errors
everywhere it's used. You can also decide to reuse the name for your new route,
and all existing links in your code will continue to just work.

If you prefer, you can also specify the path of the route you want to redirect
to:

```jsx title="Routes.jsx"
<Route path="/blog/{id}" redirect="/posts/{id}" />
<Route path="/posts/{id}" page="PostPage" name="post" />
```

## Sets of Routes

You can group Routes into sets using the `Set` component. `Set` allows you to wrap a set of Routes in another component or array of components—usually a Context, a Layout, or both:
Expand Down
58 changes: 47 additions & 11 deletions packages/router/src/__tests__/redirect.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,56 @@ import { act, render, waitFor } from '@testing-library/react'
import { navigate } from '../history'
import { Route, Router } from '../router'

const RedirectedRoutes = () => (
<Router>
<Route path="/simple" redirect="/redirectedSimple" name="simple" />
<Route
path="/redirectedSimple"
name="redirectedSimple"
page={() => <h1>FINDME</h1>}
/>
</Router>
)
const RedirectedRoutes = () => {
const SimplePage = () => <h1>FindMeSimple</h1>
const NamedPage = () => <h1>FindMeNamed</h1>
const FooBarPage = (props: unknown) => (
<>
<h1>FindMeFooBar</h1>
<pre>
<code>{JSON.stringify(props)}</code>
</pre>
</>
)

return (
<Router>
<Route path="/simple" redirect="/newSimple" name="simple" />
<Route path="/newSimple" name="newSimple" page={SimplePage} />
<Route path="/named" redirect="newNamedRoute" name="named" />
<Route path="/newNamed" name="newNamedRoute" page={NamedPage} />
<Route
path="/foobar/{foo:Int}/{bar}"
redirect="newFooBar"
name="fooBar"
/>
<Route
path="/newFooBar/{foo:Int}/{bar}"
name="newFooBar"
page={FooBarPage}
/>
</Router>
)
}

test('Redirected route', async () => {
const screen = render(<RedirectedRoutes />)
act(() => navigate('/simple'))

await waitFor(() => screen.getByText('FINDME'))
await waitFor(() => screen.getByText('FindMeSimple'))
})

test('Redirected route using route name as target', async () => {
const screen = render(<RedirectedRoutes />)
act(() => navigate('/named'))

await waitFor(() => screen.getByText('FindMeNamed'))
})

test('Redirected route using route name as target, with typed path params', async () => {
const screen = render(<RedirectedRoutes />)
act(() => navigate('/foobar/1/2'))

await waitFor(() => screen.getByText('FindMeFooBar'))
await waitFor(() => screen.getByText('{"foo":1,"bar":"2"}'))
})
24 changes: 22 additions & 2 deletions packages/router/src/router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,26 @@ const LocationAwareRouter: React.FC<RouterProps> = ({
const searchParams = parseSearch(location.search)
const allParams = { ...searchParams, ...pathParams }

let redirectPath: string | undefined = undefined

if (redirect) {
if (redirect[0] === '/') {
redirectPath = replaceParams(redirect, allParams)
} else {
const redirectRouteObject = Object.values(pathRouteMap).find(
(route) => route.name === redirect,
)

if (!redirectRouteObject) {
throw new Error(
`Redirect target route "${redirect}" does not exist for route "${name}"`,
)
}

redirectPath = replaceParams(redirectRouteObject.path, allParams)
}
}

// Level 2/3 (LocationAwareRouter)
return (
<RouterContextProvider
Expand All @@ -180,8 +200,8 @@ const LocationAwareRouter: React.FC<RouterProps> = ({
>
<ParamsProvider allParams={allParams}>
<PageLoadingContextProvider delay={pageLoadingDelay}>
{redirect && <Redirect to={replaceParams(redirect, allParams)} />}
{!redirect && page && (
{redirectPath && <Redirect to={redirectPath} />}
{!redirectPath && page && (
<WrappedPage
sets={sets}
routeLoaderElement={
Expand Down

0 comments on commit d2e5dd4

Please sign in to comment.