Routing Params
Now that we have our homepage listing all the posts, let's build the "detail" page—a canonical URL that displays a single post. First we'll generate the page and route:
yarn rw g page Article
Now let's link the title of the post on the homepage to the detail page (and include the import
for Link
and routes
):
- JavaScript
- TypeScript
import { Link, routes } from '@redwoodjs/router'
// QUERY, Loading, Empty and Failure definitions...
export const Success = ({ articles }) => {
return (
<>
{articles.map((article) => (
<article key={article.id}>
<header>
<h2>
<Link to={routes.article()}>{article.title}</Link>
</h2>
</header>
<p>{article.body}</p>
<div>Posted at: {article.createdAt}</div>
</article>
))}
</>
)
}
import { Link, routes } from '@redwoodjs/router'
// QUERY, Loading, Empty and Failure definitions...
export const Success = ({ articles }: CellSuccessProps<ArticlesQuery>) => {
return (
<>
{articles.map((article) => (
<article key={article.id}>
<header>
<h2>
<Link to={routes.article()}>{article.title}</Link>
</h2>
</header>
<p>{article.body}</p>
<div>Posted at: {article.createdAt}</div>
</article>
))}
</>
)
}
If you click the link on the title of the blog post you should see the boilerplate text on ArticlePage
:
But what we really need is to specify which post we want to view on this page. It would be nice to be able to specify the ID of the post in the URL with something like /article/1
. Let's tell the <Route>
to expect another part of the URL, and when it does, give that part a name that we can reference later:
- JavaScript
- TypeScript
<Route path="/article/{id}" page={ArticlePage} name="article" />
<Route path="/article/{id}" page={ArticlePage} name="article" />
Notice the {id}
. Redwood calls these route parameters. They say "whatever value is in this position in the path, let me reference it by the name inside the curly braces". And while we're in the routes file, lets move the route inside the Set
with the BlogLayout
.
- JavaScript
- TypeScript
import { Router, Route, Set } from '@redwoodjs/router'
import ScaffoldLayout from 'src/layouts/ScaffoldLayout'
import BlogLayout from 'src/layouts/BlogLayout'
const Routes = () => {
return (
<Router>
<Set wrap={ScaffoldLayout} title="Posts" titleTo="posts" buttonLabel="New Post" buttonTo="newPost">
<Route path="/posts/new" page={PostNewPostPage} name="newPost" />
<Route path="/posts/{id:Int}/edit" page={PostEditPostPage} name="editPost" />
<Route path="/posts/{id:Int}" page={PostPostPage} name="post" />
<Route path="/posts" page={PostPostsPage} name="posts" />
</Set>
<Set wrap={BlogLayout}>
<Route path="/article/{id}" page={ArticlePage} name="article" />
<Route path="/about" page={AboutPage} name="about" />
<Route path="/" page={HomePage} name="home" />
</Set>
<Route notfound page={NotFoundPage} />
</Router>
)
}
export default Routes
import { Router, Route, Set } from '@redwoodjs/router'
import ScaffoldLayout from 'src/layouts/ScaffoldLayout'
import BlogLayout from 'src/layouts/BlogLayout'
const Routes = () => {
return (
<Router>
<Set wrap={ScaffoldLayout} title="Posts" titleTo="posts" buttonLabel="New Post" buttonTo="newPost">
<Route path="/posts/new" page={PostNewPostPage} name="newPost" />
<Route path="/posts/{id:Int}/edit" page={PostEditPostPage} name="editPost" />
<Route path="/posts/{id:Int}" page={PostPostPage} name="post" />
<Route path="/posts" page={PostPostsPage} name="posts" />
</Set>
<Set wrap={BlogLayout}>
<Route path="/article/{id}" page={ArticlePage} name="article" />
<Route path="/about" page={AboutPage} name="about" />
<Route path="/" page={HomePage} name="home" />
</Set>
<Route notfound page={NotFoundPage} />
</Router>
)
}
export default Routes
Cool, cool, cool. Now we need to construct a link that has the ID of a post in it:
- JavaScript
- TypeScript
<h2>
<Link to={routes.article({ id: article.id })}>{article.title}</Link>
</h2>
<h2>
<Link to={routes.article({ id: article.id })}>{article.title}</Link>
</h2>
For routes with route parameters, the named route function expects an object where you specify a value for each parameter. If you click on the link now, it will indeed take you to /article/1
(or /article/2
, etc, depending on the ID of the post).
You may have noticed that when trying to view the new single-article page that you're getting an error. This is because the boilerplate code included with the page when it was generated includes a link to the page itself—a link which now requires an id
. Remove the link and your page should be working again:
- JavaScript
- TypeScript
- import { Link, routes } from '@redwoodjs/router'
import { Metadata } from '@redwoodjs/web'
const ArticlePage = () => {
return (
<>
<Metadata title="Article" description="Article page" />
<h1>ArticlePage</h1>
<p>
Find me in <code>./web/src/pages/ArticlePage/ArticlePage.js</code>
</p>
{/*
My default route is named <code>article</code>, link to me with `
<Link to={routes.article()}>Article</Link>`
*/}
</>
)
}
export default ArticlePage
- import { Link, routes } from '@redwoodjs/router'
import { Metadata } from '@redwoodjs/web'
const ArticlePage = () => {
return (
<>
<Metadata title="Article" description="Article page" />
<h1>ArticlePage</h1>
<p>
Find me in <code>./web/src/pages/ArticlePage/ArticlePage.tsx</code>
</p>
{/*
My default route is named <code>article</code>, link to me with `
<Link to={routes.article()}>Article</Link>`
*/}
</>
)
}
export default ArticlePage
Using the Param
Ok, so the ID is in the URL. What do we need next in order to display a specific post? It sounds like we'll be doing some data retrieval from the database, which means we want a cell. Note the singular Article
here since we're only displaying one:
yarn rw g cell Article
And then we'll use that cell in ArticlePage
:
- JavaScript
- TypeScript
import { Metadata } from '@redwoodjs/web'
import ArticleCell from 'src/components/ArticleCell'
const ArticlePage = () => {
return (
<>
<Metadata title="Article" description="Article page" />
<ArticleCell />
</>
)
}
export default ArticlePage
import { Metadata } from '@redwoodjs/web'
import ArticleCell from 'src/components/ArticleCell'
const ArticlePage = () => {
return (
<>
<Metadata title="Article" description="Article page" />
<ArticleCell />
</>
)
}
export default ArticlePage
Now over to the cell, we need access to that {id}
route param so we can look up the ID of the post in the database. Let's alias the real query name post
to article
and retrieve some more fields:
- JavaScript
- TypeScript
export const QUERY = gql`
query FindArticleQuery($id: Int!) {
article: post(id: $id) {
id
title
body
createdAt
}
}
`
export const Loading = () => <div>Loading...</div>
export const Empty = () => <div>Empty</div>
export const Failure = ({ error }) => (
<div style={{ color: 'red' }}>Error: {error.message}</div>
)
export const Success = ({ article }) => {
return <div>{JSON.stringify(article)}</div>
}
import type { FindArticleQuery, FindArticleQueryVariables } from 'types/graphql'
import type { CellSuccessProps, CellFailureProps } from '@redwoodjs/web'
export const QUERY = gql`
query FindArticleQuery($id: Int!) {
article: post(id: $id) {
id
title
body
createdAt
}
}
`
export const Loading = () => <div>Loading...</div>
export const Empty = () => <div>Empty</div>
export const Failure = ({ error }: CellFailureProps) => (
<div style={{ color: 'red' }}>Error: {error.message}</div>
)
export const Success = ({
article,
}: CellSuccessProps<FindArticleQuery, FindArticleQueryVariables>) => {
return <div>{JSON.stringify(article)}</div>
}
Okay, we're getting closer. Still, where will that $id
come from? Redwood has another trick up its sleeve. Whenever you put a route param in a route, that param is automatically made available to the page that route renders. Which means we can update ArticlePage
to look like this:
- JavaScript
- TypeScript
import { Metadata } from '@redwoodjs/web'
import ArticleCell from 'src/components/ArticleCell'
const ArticlePage = ({ id }) => {
return (
<>
<Metadata title="Article" description="Article page" />
<ArticleCell id={id} />
</>
)
}
export default ArticlePage
import { Metadata } from '@redwoodjs/web'
import ArticleCell from 'src/components/ArticleCell'
interface Props {
id: number
}
const ArticlePage = ({ id }: Props) => {
return (
<>
<Metadata title="Article" description="Article page" />
<ArticleCell id={id} />
</>
)
}
export default ArticlePage
id
already exists since we named our route param {id}
. Thanks Redwood! But how does that id
end up as the $id
GraphQL parameter? If you've learned anything about Redwood by now, you should know it's going to take care of that for you. By default, any props you give to a cell will automatically be turned into variables and given to the query. "No way," you're saying. Way.
We can prove it! Try going to the detail page for a post in the browser and—uh oh. Hmm:
This error message you're seeing is thanks to the Failure
section of our Cell!
Error: Variable "$id" got invalid value "1"; Int cannot represent non-integer value: "1"
It turns out that route params are extracted as strings from the URL, but GraphQL wants an integer for the id
. We could use parseInt()
to convert it to a number before passing it into ArticleCell
, but we can do better than that.
Route Param Types
What if you could request the conversion right in the route's path? Introducing route param types. It's as easy as adding :Int
to our existing route param:
- JavaScript
- TypeScript
<Route path="/article/{id:Int}" page={ArticlePage} name="article" />
<Route path="/article/{id:Int}" page={ArticlePage} name="article" />
Voilà! Not only will this convert the id
param to a number before passing it to your Page, it will prevent the route from matching unless the id
path segment consists entirely of digits. If any non-digits are found, the router will keep trying other routes, eventually showing the NotFoundPage
if no routes match.
All of the props you give to the cell will be automatically available as props in the render components. Only the ones that match the GraphQL variables list will be given to the query. You get the best of both worlds! In our post display above, if you wanted to display some random number along with the post (for some contrived, tutorial-like reason), just pass that prop:
- JavaScript
- TypeScript
<ArticleCell id={id} rand={Math.random()} />
<ArticleCell id={id} rand={Math.random()} />
And get it, along with the query result (and even the original id
if you want) in the component:
- JavaScript
- TypeScript
export const Success = ({ article, id, rand }) => {
// ...
}
interface Props
extends CellSuccessProps<FindArticleQuery, FindArticleQueryVariables> {
id: number
rand: number
}
export const Success = ({ article, id, rand }: Props) => {
// ...
}
Thanks again, Redwood!
Displaying a Blog Post
Now let's display the actual post instead of just dumping the query result. We could copy the display from the articles on the homepage, but that's not very reusable! This is the perfect place for a good old fashioned component—define the display once and then reuse the component on the homepage and the article display page. Both ArticlesCell
and ArticleCell
will display our new component. Let's Redwood-up a component (I just invented that phrase):
yarn rw g component Article
Which creates web/src/components/Article/Article.tsx
(and corresponding test and more!) as a super simple React component:
- JavaScript
- TypeScript
const Article = () => {
return (
<div>
<h2>{'Article'}</h2>
<p>{'Find me in ./web/src/components/Article/Article.jsx'}</p>
</div>
)
}
export default Article
const Article = () => {
return (
<div>
<h2>{'Article'}</h2>
<p>{'Find me in ./web/src/components/Article/Article.tsx'}</p>
</div>
)
}
export default Article
You may notice we don't have any explicit import
statements for React
itself. We (the Redwood dev team) got tired of constantly importing it over and over again in every file so we automatically import it for you!
Let's copy the <article>
section from ArticlesCell
and put it here instead, taking the article
itself in as a prop:
- JavaScript
- TypeScript
import { Link, routes } from '@redwoodjs/router'
const Article = ({ article }) => {
return (
<article>
<header>
<h2>
<Link to={routes.article({ id: article.id })}>{article.title}</Link>
</h2>
</header>
<div>{article.body}</div>
<div>Posted at: {article.createdAt}</div>
</article>
)
}
export default Article
import { Link, routes } from '@redwoodjs/router'
import type { Post } from 'types/graphql'
interface Props {
article: Post
}
const Article = ({ article }: Props) => {
return (
<article>
<header>
<h2>
<Link to={routes.article({ id: article.id })}>{article.title}</Link>
</h2>
</header>
<div>{article.body}</div>
<div>Posted at: {article.createdAt}</div>
</article>
)
}
export default Article
And update ArticlesCell
to use this new component instead:
- JavaScript
- TypeScript
import Article from 'src/components/Article'
export const QUERY = gql`
query ArticlesQuery {
articles: posts {
id
title
body
createdAt
}
}
`
export const Loading = () => <div>Loading...</div>
export const Empty = () => <div>Empty</div>
export const Failure = ({ error }) => (
<div style={{ color: 'red' }}>Error: {error.message}</div>
)
export const Success = ({ articles }) => {
return (
<>
{articles.map((article) => (
<Article key={article.id} article={article} />
))}
</>
)
}
import Article from 'src/components/Article'
import type { ArticlesQuery } from 'types/graphql'
import type { CellSuccessProps, CellFailureProps } from '@redwoodjs/web'
export const QUERY = gql`
query ArticlesQuery {
articles: posts {
id
title
body
createdAt
}
}
`
export const Loading = () => <div>Loading...</div>
export const Empty = () => <div>Empty</div>
export const Failure = ({ error }: CellFailureProps) => (
<div style={{ color: 'red' }}>Error: {error.message}</div>
)
export const Success = ({ articles }: CellSuccessProps<ArticlesQuery>) => {
return (
<>
{articles.map((article) => (
<Article key={article.id} article={article} />
))}
</>
)
}
Last but not least we can update the ArticleCell
to properly display our blog posts as well:
- JavaScript
- TypeScript
import Article from 'src/components/Article'
export const QUERY = gql`
query FindArticleQuery($id: Int!) {
article: post(id: $id) {
id
title
body
createdAt
}
}
`
export const Loading = () => <div>Loading...</div>
export const Empty = () => <div>Empty</div>
export const Failure = ({ error }) => (
<div style={{ color: 'red' }}>Error: {error.message}</div>
)
export const Success = ({ article }) => {
return <Article article={article} />
}
import Article from 'src/components/Article'
import type { FindArticleQuery, FindArticleQueryVariables } from 'types/graphql'
import type { CellSuccessProps, CellFailureProps } from '@redwoodjs/web'
export const QUERY = gql`
query FindArticleQuery($id: Int!) {
article: post(id: $id) {
id
title
body
createdAt
}
}
`
export const Loading = () => <div>Loading...</div>
export const Empty = () => <div>Empty</div>
export const Failure = ({ error }: CellFailureProps) => (
<div style={{ color: 'red' }}>Error: {error.message}</div>
)
export const Success = ({ article }: CellSuccessProps<FindArticleQuery, FindArticleQueryVariables>) => {
return <Article article={article} />
}
And there we go! We should be able to move back and forth between the homepage and the detail page. If you've only got one blog post then the homepage and single-article page will be identical! Head to the posts admin and create a couple more, won't you?
If you like what you've been seeing from the router, you can dive deeper into the Redwood Router guide.
Summary
To recap:
- We created a new page to show a single post (the "detail" page).
- We added a route to handle the
id
of the post and turn it into a route param, even coercing it into an integer. - We created a cell to fetch and display the post.
- Redwood made the world a better place by making that
id
available to us at several key junctions in our code and even turning it into a number automatically. - We turned the actual post display into a standard React component and used it in both the homepage and new detail page.