Routing Params
すべての投稿を表示するホームページができたので "deatil" (詳細)ページ -- つまり単一のブログ記事を表示する正規の URL を作成しましょう。まず、ページとルートを生成します:
yarn rw g page Article
では、ホームページのブログ記事のタイトルを詳細ページにリンクしてみましょう( Link
と routes
は import
します):
- 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>
))}
</>
)
}
ブログ記事のタイトルのリンクをクリックすると、 ArticlePage
に定型文が表示されます:
しかし本当に必要なのは、このページで表示したい どの ブログ記事を指定するかということです。URLの中で /article/1
のようにブログ記事のIDを指定できればいいのですが。 <Route>
にURLの別の部分を期待するように伝え、それがきたら、その部分に後で参照できるような名前をつけましょう:
- JavaScript
- TypeScript
<Route path="/article/{id}" page={ArticlePage} name="article" />
<Route path="/article/{id}" page={ArticlePage} name="article" />
{id}
に注目してください。Redwoodではこれを ルートパラメータ と呼んでいます。これは "パスのこの位置にある値が何であれ、中括弧の中の名前で参照させてください" と言う意味です。そしてルートファイルで、ルートを Set
内の 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
イイネ、イイネ、イィーネッ。 さて、次はブログ記事のIDを含むリンクを構築しなければなりません:
- 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>
ルートパラメータを持つルートの場合、名前付きルート関数は、各パラメータの値を指定するオブジェクトをが渡されることを期待します。今リンクをクリックすると、確かに /article/1
(または /article/2
など、ブログ記事のIDによります)に移動します。
新しいブログ記事詳細ページを表示しようとすると、エラーが発生することにお気づきかもしれません。これは、ページが生成されたときの定型的なコードに、ページ自身へのリンクが含まれているためで、このリンクには id
が必要です。このリンクを削除すれば、ページは再び動作するようになります:
- JavaScript
- TypeScript
- import { Link, routes } from '@redwoodjs/router'
import { MetaTags } from '@redwoodjs/web'
const ArticlePage = () => {
return (
<>
<MetaTags title="Article" description="Article page" />
<h1>ArticlePage</h1>
<p>
Find me in <code>./web/src/pages/ArticlePage/ArticlePage.js</code>
</p>
<p>
My default route is named <code>article</code>, link to me with `
- <Link to={routes.article()}>Article</Link>`
</p>
</>
)
}
export default ArticlePage
- import { Link, routes } from '@redwoodjs/router'
import { MetaTags } from '@redwoodjs/web'
const ArticlePage = () => {
return (
<>
<MetaTags title="Article" description="Article page" />
<h1>ArticlePage</h1>
<p>
Find me in <code>./web/src/pages/ArticlePage/ArticlePage.tsx</code>
</p>
<p>
My default route is named <code>article</code>, link to me with `
- <Link to={routes.article()}>Article</Link>`
</p>
</>
)
}
export default ArticlePage
Using the Param
OK、IDはURLの中にあります。特定のブログ記事を表示するために次に必要なものは何でしょうか?データベースからデータを取得するようですが、これはつまりセルが必要だということです。ここでは1つしか表示しないので、単数形の Article
となっている点に注意してください:
yarn rw g cell Article
そうしたら、このセルを ArticlePage
で使用します:
- JavaScript
- TypeScript
import { MetaTags } from '@redwoodjs/web'
import ArticleCell from 'src/components/ArticleCell'
const ArticlePage = () => {
return (
<>
<MetaTags title="Article" description="Article page" />
<ArticleCell />
</>
)
}
export default ArticlePage
import { MetaTags } from '@redwoodjs/web'
import ArticleCell from 'src/components/ArticleCell'
const ArticlePage = () => {
return (
<>
<MetaTags title="Article" description="Article page" />
<ArticleCell />
</>
)
}
export default ArticlePage
今度はセルの方ですが、データベースにあるブログ記事のIDを調べるために {id}
ルートパラメータにアクセスする必要があります。本当のクエリ名 post
を article
にエイリアスして、さらにいくつかのフィールドを取得しましょう:
- JavaScript
- TypeScript
export const QUERY = gql`
query ArticleQuery($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 JSON.stringify(article)
}
import type { ArticleQuery } from 'types/graphql'
import type { CellSuccessProps, CellFailureProps } from '@redwoodjs/web'
export const QUERY = gql`
query ArticleQuery($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<ArticleQuery>) => {
return JSON.stringify(article)
}
OK。近づいてきました。それにしても、あの$id
はどこからきたのでしょう?Redwoodにはもう一つトリックがあります。ルート内にルートパラメータを置くと、そのパラメータはルートがレンダリングするページで自動的に利用できるようになります。つまり ArticlePage
を更新して次のようにできます:
- JavaScript
- TypeScript
import { MetaTags } from '@redwoodjs/web'
import ArticleCell from 'src/components/ArticleCell'
const ArticlePage = ({ id }) => {
return (
<>
<MetaTags title="Article" description="Article page" />
<ArticleCell id={id} />
</>
)
}
export default ArticlePage
import { MetaTags } from '@redwoodjs/web'
import ArticleCell from 'src/components/ArticleCell'
interface Props {
id: number
}
const ArticlePage = ({ id }: Props) => {
return (
<>
<MetaTags title="Article" description="Article page" />
<ArticleCell id={id} />
</>
)
}
export default ArticlePage
ルートパラメータに {id}
という名前をつけたので id
はすでに存在しています。Redwoodありがとう!しかしこの id
はどのようにして $id
という GraphQL パラメータになるのでしょうか?もしあなたがRedwoodについて学んだことがあるなら、Redwoodがあなたのためにそれを処理してくれていることを知るべきです。デフォルトでは、セルに与えた props は自動的に変数に変換され、クエリに渡されます。"マジか" と思うでしょう?マジです。
証明してあげます!ブラウザでブログ記事の詳細ページを見てみると......oh......んーむ:
いま見えているエラーメッセージは、セルの Failure
セクションのおかげです!
Error: Variable "$id" got invalid value "1"; Int cannot represent non-integer value: "1"
ルートパラメータはURLから文字列として抽出されますが、GraphQLは id
に整数を要求していることがわかりました。 ArticleCell
に渡す前に parseInt()
を使って数値に変換することもできますが、それよりもよい方法があります。
Route Param Types
もし、ルートのパスで型変換を要求できるとしたらどうでしょう? route param types を紹介します。既存のルートパラメータに :Int
を追加するのと同じくらい簡単です:
- JavaScript
- TypeScript
<Route path="/article/{id:Int}" page={ArticlePage} name="article" />
<Route path="/article/{id:Int}" page={ArticlePage} name="article" />
ほらね!これは id
パラメータを数値に変換してからページに渡すだけでなく、 id
パスセグメントが数字だけで構成されていなければルートにマッチしないようにします。もし数字以外が見つかった場合、ルータは他のルートを試し、最終的にマッチするルートがない場合は NotFoundPage
を表示します。
セルに与えたすべての props は、自動的にレンダーコンポーネントの props として利用できるようになります。GraphQLの変数リストにマッチするものだけがクエリに渡されます。あなたは両方の世界のベストを得ることができます!上記のブログ記事表示で、一緒に乱数を表示したい場合(チュートリアル的な意図的な理由で)、その props を渡せばいいのです:
- JavaScript
- TypeScript
<ArticleCell id={id} rand={Math.random()} />
<ArticleCell id={id} rand={Math.random()} />
そして、クエリ結果(と、必要なら元の id
も)と一緒にコンポーネントに渡します:
- JavaScript
- TypeScript
export const Success = ({ article, id, rand }) => {
// ...
}
interface Props extends CellSuccessProps<ArticleQuery> {
id: number
rand: number
}
export const Success = ({ article, id, rand }: Props) => {
// ...
}
Redwood またまたありがとう!
Displaying a Blog Post
さて、クエリ結果をダンプする代わりに、実際のブログ記事を表示してみましょう。ホームページの記事からコピーすることもできますが、それはあまり再利用性がありません!これは古き良き時代のコンポーネントのための完璧な場所です - 表示を一度定義して、ホームページとブログ記事詳細ページでそのコンポーネントを再利用します。 ArticlesCell
と ArticleCell
の両方で新しいコンポーネントが表示されます。コンポーネントをRedwood-upしてみましょう(このフレーズは今作りました):
yarn rw g component Article
これは web/src/components/Article/Article.tsx
(と、対応するテストなど!)を超シンプルなReactコンポーネントとして作成します:
- JavaScript
- TypeScript
const Article = () => {
return (
<div>
<h2>{'Article'}</h2>
<p>{'Find me in ./web/src/components/Article/Article.js'}</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
React
自体には明示的な import
ステートメントがないことにお気づきかもしれません。私たち(Redwood 開発チーム)は、すべてのファイルで何度も何度もインポートするのに疲れてしまったので、あなたのために自動的にインポートするようにしました!
ArticlesCell
から <article>
セクションをコピーして、代わりに article
自体を props として取り込んでここに配置しましょう:
- 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
そうしたら、代わりにこの新しいコンポーネントを使用するよう ArticlesCell
を更新しましょう:
- 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} />
))}
</>
)
}
最後に、ブログ記事を適切に表示するために ArticleCell
を更新できます:
- JavaScript
- TypeScript
import Article from 'src/components/Article'
export const QUERY = gql`
query ArticleQuery($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 { ArticleQuery } from 'types/graphql'
import type { CellSuccessProps, CellFailureProps } from '@redwoodjs/web'
export const QUERY = gql`
query ArticleQuery($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<ArticleQuery>) => {
return <Article article={article} />
}
よし、いいぞ!ホームページと詳細ページを行き来できるようになりました。ブログの記事が1つしかない場合は、トップページと詳細ページが同じになります!ブログ記事管理画面へ移動して、もう2つほど作成してみましょうかね?
ルータが気になるなら Redwood Router ガイドにdeep diveできますよ。
Summary
おさらい:
- 一つのブログ記事を表示するページ(詳細ページ)を新規作成した
- ブログ記事の
id
を処理するルートを追加し、それをルートパラメータに変換し、さらにそれを整数に固定した - ブログ記事を取得して表示するセルを作成した
- Redwoodは、コードのいくつかの重要な分岐点で
id
を利用できるようにし、さらにそれを自動的に数値に変換することで、世界をより良い場所にした - 実際のブログ記事表示は標準的なReactコンポーネントとし、ホームページと新しい詳細ページの両方で使用した