Table of Contents
はじめに
Chapter 2ではCRUD処理を自動生成するscaffoldやデータの取得表示を行うセルについて主に学んでいきます。
前回の記事
データベーススキーマ作成
RedwoodJSではデータベースに関する部分はPrismaを採用しています。 すでにschema.prismaファイルが用意されていますので、こちらを編集してテーブルを作成していきます。
// api/db/schema.prisma
datasource db {
provider = "sqlite"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
binaryTargets = "native"
}
model Post {
id Int @id @default(autoincrement())
title String
body String
createdAt DateTime @default(now())
}
Postテーブルを追加しました。
マイグレーション
スキーマができたらマイグレーションコマンドを実行します。
yarn rw prisma migrate dev
Running Prisma CLI...
$ yarn prisma migrate dev --schema /xxx/xxx/xxx/my-first-redwood-app/api/db/schema.prisma
Environment variables loaded from .env
Prisma schema loaded from api/db/schema.prisma
Datasource "db": SQLite database "dev.db" at "file:./dev.db"
SQLite database dev.db created at file:./dev.db
✔ Enter a name for the new migration: … create post
Applying migration `xxxxxxxxx_create_post`
The following migration(s) have been created and applied from new schema changes:
migrations/
└─ xxxxxxxxx_create_post/
└─ migration.sql
Your database is now in sync with your schema.
✔ Generated Prisma Client (v5.14.0) to ./node_modules/@prisma/client in 30ms
Running seed command `yarn rw exec seed` ...
[STARTED] Generating Prisma client
[COMPLETED] Generating Prisma client
[STARTED] Running script
No seed data, skipping. See scripts/seed.ts to start seeding your database!
[COMPLETED] Running script
🌱 The seed command has been executed.
Prisma Studio
無事にテーブルが作られたか確認するためにPrisma Studioを立ち上げます。
yarn rw prisma studio
ここからレコードの追加なども行えるので大変便利です。
Scaffolding
scaffold
コマンドを使って、PostのCRUDを作ります。
yarn rw g scaffold post
✔ Generating scaffold files...
✔ Successfully wrote file `./web/src/components/Post/EditPostCell/EditPostCell.tsx`
✔ Successfully wrote file `./web/src/components/Post/Post/Post.tsx`
✔ Successfully wrote file `./web/src/components/Post/PostCell/PostCell.tsx`
✔ Successfully wrote file `./web/src/components/Post/PostForm/PostForm.tsx`
✔ Successfully wrote file `./web/src/components/Post/Posts/Posts.tsx`
✔ Successfully wrote file `./web/src/components/Post/PostsCell/PostsCell.tsx`
✔ Successfully wrote file `./web/src/components/Post/NewPost/NewPost.tsx`
✔ Successfully wrote file `./api/src/graphql/posts.sdl.ts`
✔ Successfully wrote file `./api/src/services/posts/posts.ts`
✔ Successfully wrote file `./api/src/services/posts/posts.scenarios.ts`
✔ Successfully wrote file `./api/src/services/posts/posts.test.ts`
✔ Successfully wrote file `./web/src/scaffold.css`
✔ Successfully wrote file `./web/src/lib/formatters.tsx`
✔ Successfully wrote file `./web/src/lib/formatters.test.tsx`
✔ Successfully wrote file `./web/src/layouts/ScaffoldLayout/ScaffoldLayout.tsx`
✔ Successfully wrote file `./web/src/pages/Post/EditPostPage/EditPostPage.tsx`
✔ Successfully wrote file `./web/src/pages/Post/PostPage/PostPage.tsx`
✔ Successfully wrote file `./web/src/pages/Post/PostsPage/PostsPage.tsx`
✔ Successfully wrote file `./web/src/pages/Post/NewPostPage/NewPostPage.tsx`
✔ Install helper packages
✔ Adding layout import...
✔ Adding set import...
✔ Adding scaffold routes...
✔ Adding scaffold asset imports...
✔ Generating types ...
色々なファイルが作成されました。 ここでは各ファイルについて解説しません。詳しくは公式Tutorialのページを参照してください。
ざっくりいうと、CRUDに必要な以下の内容のファイル作成または変更がおこなわれました。
- ページの作成
- レイアウトの作成
- ルーティングの追加
- セルの作成(この後説明)
- コンポーネントの作成(フォームやテーブルなど)
- SDL(GraphQL関連)の作成
- サービスの作成(PrismaからDBへのアクセス処理)
実際に/posts
へアクセスしてみると、Postの一覧が表示され、そこから作成、詳細、編集、削除ができました。
セル(Cells)
セルはクライアント側のデータフェッチに関する処理を書いたRedwoodJS独自の手法で、GraphQLクエリを書いてデータを取得します。
先ほどscaffoldしたファイルの1つであるPostsCell.tsxを見てみます。
import type { FindPosts, FindPostsVariables } from 'types/graphql'
import { Link, routes } from '@redwoodjs/router'
import type {
CellSuccessProps,
CellFailureProps,
TypedDocumentNode,
} from '@redwoodjs/web'
import Posts from 'src/components/Post/Posts'
export const QUERY: TypedDocumentNode<FindPosts, FindPostsVariables> = gql`
query FindPosts {
posts {
id
title
body
createdAt
}
}
`
export const Loading = () => <div>Loading...</div>
export const Empty = () => {
return (
<div className="rw-text-center">
{'No posts yet. '}
<Link to={routes.newPost()} className="rw-link">
{'Create one?'}
</Link>
</div>
)
}
export const Failure = ({ error }: CellFailureProps<FindPosts>) => (
<div className="rw-cell-error">{error?.message}</div>
)
export const Success = ({
posts,
}: CellSuccessProps<FindPosts, FindPostsVariables>) => {
return <Posts posts={posts} />
}
それぞれexportしているものを見ていきます。
Query
: GraphQLクエリLoading
: レスポンスを受け取るまで表示されるコンポーネントEmpty
: データが空(nullまたは空配列)だった場合に表示されるコンポーネントFailure
: エラーだった場合に表示されるコンポーネントSuccess
: データ取得に成功した場合に表示されるコンポーネント
他にもクエリ実行前後の処理が書けるbeforeQuery
やafterQuery
というライフサイクルヘルパーというものがあるそうです。
次にセルコンポーネントのみ生成してみます。
yarn rw g cell Articles
Generating cell files...
✔ Successfully wrote file `./web/src/components/ArticlesCell/ArticlesCell.mock.ts`
✔ Successfully wrote file `./web/src/components/ArticlesCell/ArticlesCell.test.tsx`
✔ Successfully wrote file `./web/src/components/ArticlesCell/ArticlesCell.stories.tsx`
✔ Successfully wrote file `./web/src/components/ArticlesCell/ArticlesCell.tsx`
↓ Skipping type generation: no SDL defined for "articles". To generate types, run 'yarn rw g sdl articles'.
scaffoldコマンドの時はweb/src/components/Postディレクトリ以下にさらにPostCellディレクトリやPostsCellディレクトリなどができましたが、今回はweb/src/components/ArticlesCellディレクトリが直接できました。
そこにあるArticlesCell.tsxの中身を見てみます。
import type { ArticlesQuery, ArticlesQueryVariables } from 'types/graphql'
import type {
CellSuccessProps,
CellFailureProps,
TypedDocumentNode,
} from '@redwoodjs/web'
export const QUERY: TypedDocumentNode<
ArticlesQuery,
ArticlesQueryVariables
> = gql`
query ArticlesQuery {
articles {
id
}
}
`
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 (
<ul>
{articles.map((item) => {
return <li key={item.id}>{JSON.stringify(item)}</li>
})}
</ul>
)
}
PostsCell.tsxと似たようなコードになっていますね。
ただしVSCodeエディタなど使ってる方は気づくかもしれませんが、いくつか型エラーが出ています。
ここでGraphQLクエリの箇所を以下のように変更してみます。
export const QUERY: TypedDocumentNode<
ArticlesQuery,
ArticlesQueryVariables
> = gql`
query ArticlesQuery {
articles {
id
}
}
`
クエリをarticles
からposts
にしました。
すると開発サーバーを起動している方は上記変更を検知してimportしているtypes/graphqlの型が生成されます。
これはセルのみを今回は生成したので、articlesクエリというSDL(GraphQLのスキーマを定義するファイル)やService(ビジネスロジックやデータ操作を行うファイル)が存在しないため起こったエラーです。
すでにscaffoldで存在しているpostsクエリに書き直すことでエラーがなくなりました。
無事クエリエラーを解決したので、Successコンポーネントを以下のように変更してデータを表示させてみます。
export const Success = ({ posts }: CellSuccessProps<ArticlesQuery>) => {
return (
<ul>
{posts.map((item) => {
return <li key={item.id}>{JSON.stringify(item)}</li>
})}
</ul>
)
}
HomePage.tsxにArticlesCellを表示させます。
import { Metadata } from '@redwoodjs/web'
import ArticlesCell from 'src/components/ArticlesCell'
const HomePage = () => {
return (
<>
<Metadata title="Home" description="Home page" />
<ArticlesCell />
</>
)
}
export default HomePage
ブラウザで確認してみるとうまくデータが表示されているのがわかります。
以下のようにGraphQLクエリの結果に含まれる変数名にエイリアスを使うこともできます。
export const QUERY: TypedDocumentNode<
ArticlesQuery,
ArticlesQueryVariables
> = gql`
query ArticlesQuery {
articles: posts {
id
}
}
`
export const Success = ({ articles }: CellSuccessProps<ArticlesQuery>) => {
return (
<ul>
{articles.map((item) => {
return <li key={item.id}>{JSON.stringify(item)}</li>
})}
</ul>
)
}
最後に取得項目を増やして見た目を整えたら完成です。
import type { ArticlesQuery, ArticlesQueryVariables } from 'types/graphql'
import type {
CellSuccessProps,
CellFailureProps,
TypedDocumentNode,
} from '@redwoodjs/web'
export const QUERY: TypedDocumentNode<
ArticlesQuery,
ArticlesQueryVariables
> = 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}>
<header>
<h2>{article.title}</h2>
</header>
<p>{article.body}</p>
<div>Posted at: {article.createdAt}</div>
</article>
))}
</>
)
}
ルーティングパラメータ
ホームにPost一覧の表示できたので、次は詳細画面を作成してみます。
詳細ページ作成
まずは以下コマンドで詳細ページを作成します。
yarn rw g page Article
✔ Generating page files...
✔ Successfully wrote file `./web/src/pages/ArticlePage/ArticlePage.stories.tsx`
✔ Successfully wrote file `./web/src/pages/ArticlePage/ArticlePage.test.tsx`
✔ Successfully wrote file `./web/src/pages/ArticlePage/ArticlePage.tsx`
✔ Updating routes file...
✔ Generating types...
✔ One more thing...
Page created! A note about <Metadata>:
At the top of your newly created page is a <Metadata> component,
which contains the title and description for your page, essential
to good SEO. Check out this page for best practices:
https://developers.google.com/search/docs/advanced/appearance/good-titles-snippets
ArticleCell.tsxに詳細ページへのリンクを追加します。
export const Success = ({ articles }: CellSuccessProps<ArticlesQuery>) => {
return (
<>
{articles.map((article) => (
<article key={article.id}>
<header>
<Link to={routes.article()}>{article.title}</Link>
</header>
<p>{article.body}</p>
<div>Posted at: {article.createdAt}</div>
</article>
))}
</>
)
}
そして、今回はURLを/article/1
のようにIDを含めたいので、自動追加されたArticlePageのルートを編集します。
// Routes.tsx
<Route path="/article/{id}" page={ArticlePage} name="article" />
pathに{id}
を加えことでリンクの方もパラメータを渡すことができます。
// web/src/components/ArticlesCell/ArticlesCell.tsx
<h2>
<Link to={routes.article({ id: article.id })}>{article.title}</Link>
</h2>
Note
ここで実際route.article
で待ち受けているidとarticle.id
は型が違って(stringとnumber)TSエラーになります。公式Tutorialでは一旦エラーを無視して良いと書かれています。
次のセクションでこの部分について説明があるようです。
仕上げにArticlePage.tsxのリンクも不要なので削除します。
// web/src/pages/ArticlePage.tsx
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>
<p>
My default route is named <code>article</code>, link to me with `
</p>
</>
)
}
export default ArticlePage
これで詳細ページ遷移時にブラウザのURLを確認すると/article/1
のような値になっているはずです。
詳細データ取得表示
ページが用意できたので、セルを作成して詳細データの取得表示を行います。
yarn rw g cell Article
✔ Generating cell files...
✔ Successfully wrote file `./web/src/components/ArticleCell/ArticleCell.mock.ts`
✔ Successfully wrote file `./web/src/components/ArticleCell/ArticleCell.test.tsx`
✔ Successfully wrote file `./web/src/components/ArticleCell/ArticleCell.stories.tsx`
✔ Successfully wrote file `./web/src/components/ArticleCell/ArticleCell.tsx`
↓ Skipping type generation: no SDL defined for "article". To generate types, run 'yarn rw g sdl article'.
生成されたArticleCellをArticlePageで表示します。
// web/src/pages/ArticlePage/ArticlePage.tsx
import { Metadata } from '@redwoodjs/web'
import ArticleCell from 'src/components/ArticleCell'
const ArticlePage = () => {
return (
<>
<Metadata title="Article" description="Article page" />
<ArticleCell />
</>
)
}
export default ArticlePage
ArticleCellの方もArticlesCellの時と同様に
export const QUERY: TypedDocumentNode<
FindArticleQuery,
FindArticleQueryVariables
> = gql`
query FindArticleQuery($id: Int!) {
article: article(id: $id) {
id
}
}
`
を
export const QUERY: TypedDocumentNode<
FindArticleQuery,
FindArticleQueryVariables
> = gql`
query FindArticleQuery($id: Int!) {
article: post(id: $id) {
id
title
body
createdAt
}
}
`
に変更します。 ついでにtitle/body/createdAtを追加で取得するようにしました。
そして、ArticleCellにidを渡さないといけないので、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
これで完成かと思い、ブラウザで動作確認してみると詳細画面で以下のようなエラーが出ます。
Error: Response not successful: Received status code 400
これは先ほどのTSエラーと関係していて、GraphQLではidをIntegerとして待ち受けていましたが、実際はStringで渡ってきてしまっていたために起こったエラーです。
これを解決するためにはRoute Param Typesというものを使います。 Routes.tsxの詳細ページ部分を以下のように書き換えます。
<Route path="/article/{id:Int}" page={ArticlePage} name="article" />
{id:Int}
に変わりました。これによりidがIntegerに変換されるだけでなく、Integerではない値が来た場合はNotFoundPageが表示されるようになります。
これで前回のTSエラーも解消され、無事に詳細画面が見れるようになりました。
コンポーネントの作成
最後に再利用可能なコンポーネントを作成してArticlesCellとArticleCellの両方で使えるようにします。
yarn rw g component Article
✔ Generating component files...
✔ Successfully wrote file `./web/src/components/Article/Article.test.tsx`
✔ Successfully wrote file `./web/src/components/Article/Article.stories.tsx`
✔ Successfully wrote file `./web/src/components/Article/Article.tsx`
生成されたweb/src/components/Article/Article.tsxの中身は以下です。
const Article = () => {
return (
<div>
<h2>{'Article'}</h2>
<p>{'Find me in ./web/src/components/Article/Article.tsx'}</p>
</div>
)
}
export default Article
ArticlesCellのarticleタグ部分をArticleコンポーネントに持ってきます。
import type { Post } from 'types/graphql'
import { Link, routes } from '@redwoodjs/router'
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.tsx側もArticleコンポーネントに差し替えます。
import type { ArticlesQuery, ArticlesQueryVariables } from 'types/graphql'
import type {
CellSuccessProps,
CellFailureProps,
TypedDocumentNode,
} from '@redwoodjs/web'
import Article from 'src/components/Article/Article'
export const QUERY: TypedDocumentNode<
ArticlesQuery,
ArticlesQueryVariables
> = 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.tsxも同じくArticleコンポーネントを使うようにします。
import type { FindArticleQuery, FindArticleQueryVariables } from 'types/graphql'
import type {
CellSuccessProps,
CellFailureProps,
TypedDocumentNode,
} from '@redwoodjs/web'
import Article from 'src/components/Article'
export const QUERY: TypedDocumentNode<
FindArticleQuery,
FindArticleQueryVariables
> = 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<FindArticleQueryVariables>) => (
<div style={{ color: 'red' }}>Error: {error?.message}</div>
)
export const Success = ({
article,
}: CellSuccessProps<FindArticleQuery, FindArticleQueryVariables>) => {
return <Article article={article} />
}
これで一覧画面と詳細画面で共通した見た目を持つことができました。 Chapter 2はここまでになります。