Skip to main content
Version: 5.0

Creating a Comment Form

新しいコメントフォームを収めるコンポーネントを生成し、それを構築してStorybook経由で統合し、いくつかのテストを追加してみましょう:

yarn rw g component CommentForm

そして、いま起動していない場合は、改めてStorybookを起動してください:

yarn rw storybook

Storybookに CommentForm という項目があり、すぐに始められるようになっているのがわかると思います。

image

Storybook

ユーザの名前とコメントを受け取る簡単なフォームを作り、ブログとマッチするようにスタイルを追加してみましょう:

web/src/components/CommentForm/CommentForm.js
import {
Form,
Label,
TextField,
TextAreaField,
Submit,
} from '@redwoodjs/forms'

const CommentForm = () => {
return (
<div>
<h3 className="font-light text-lg text-gray-600">Leave a Comment</h3>
<Form className="mt-4 w-full">
<Label name="name" className="block text-sm text-gray-600 uppercase">
Name
</Label>
<TextField
name="name"
className="block w-full p-1 border rounded text-xs "
validation={{ required: true }}
/>

<Label
name="body"
className="block mt-4 text-sm text-gray-600 uppercase"
>
Comment
</Label>
<TextAreaField
name="body"
className="block w-full p-1 border rounded h-24 text-xs"
validation={{ required: true }}
/>

<Submit
className="block mt-4 bg-blue-500 text-white text-xs font-semibold uppercase tracking-wide rounded px-3 py-2 disabled:opacity-50"
>
Submit
</Submit>
</Form>
</div>
)
}

export default CommentForm

image

フォームとその入力フィールドは100%の幅に設定されていることに注意してください。繰り返しますが、フォームのレイアウトについては、入力の幅のように親が責任を負うべきことを指定すべきではありません。

フォームのレイアウトはそのフォームを親が決めるべきで、ページ上の他のコンテンツと揃えて見栄えをよくする必要があります。 ですから、フォームの幅は100%で、ページ上での実際の幅は(親が誰であろうと)親が決めることになります。

Storybookでフォームを送信してみることもできます!もし、"name" や "comment" を空白のままにした場合、送信しようとすると、それらが必須であることを示すフォーカスが当たるはずです。両方を入力して Submit をクリックしても、まだ送信処理を繋ぎ込んでいないので何も起こりません。では、それをやってみましょう。

Submitting

フォームを送信するには、サービスとGraphQLに追加した createComment 関数を使う必要があります。フォームのデータで create を呼び出せるように、フォームコンポーネントにミューテーションを追加し、 onSubmit ハンドラを追加する必要があります。また、createComment がエラーを返す可能性があるので、それを表示するために FormError コンポーネントを追加します:

web/src/components/CommentForm/CommentForm.js
import {
Form,
FormError,
Label,
TextField,
TextAreaField,
Submit,
} from '@redwoodjs/forms'
import { useMutation } from '@redwoodjs/web'

const CREATE = gql`
mutation CreateCommentMutation($input: CreateCommentInput!) {
createComment(input: $input) {
id
name
body
createdAt
}
}
`

const CommentForm = () => {
const [createComment, { loading, error }] = useMutation(CREATE)

const onSubmit = (input) => {
createComment({ variables: { input } })
}

return (
<div>
<h3 className="font-light text-lg text-gray-600">Leave a Comment</h3>
<Form className="mt-4 w-full" onSubmit={onSubmit}>
<FormError
error={error}
titleClassName="font-semibold"
wrapperClassName="bg-red-100 text-red-900 text-sm p-3 rounded"
/>
<Label
name="name"
className="block text-xs font-semibold text-gray-500 uppercase"
>
Name
</Label>
<TextField
name="name"
className="block w-full p-1 border rounded text-sm "
validation={{ required: true }}
/>

<Label
name="body"
className="block mt-4 text-xs font-semibold text-gray-500 uppercase"
>
Comment
</Label>
<TextAreaField
name="body"
className="block w-full p-1 border rounded h-24 text-sm"
validation={{ required: true }}
/>

<Submit
disabled={loading}
className="block mt-4 bg-blue-500 text-white text-xs font-semibold uppercase tracking-wide rounded px-3 py-2 disabled:opacity-50"
>
Submit
</Submit>
</Form>
</div>
)
}

export default CommentForm

フォームを送信しようとすると、ウェブコンソールにエラーが表示されます -- Storybookは自動的にGraphQLクエリをモックしますが、ミューテーションはモックしません。しかし、ストーリーの中でリクエストをモックし、レスポンスを手動で処理することができます:

web/src/components/CommentForm/CommentForm.stories.js
import CommentForm from './CommentForm'

export const generated = () => {
mockGraphQLMutation('CreateCommentMutation', (variables, { ctx }) => {
const id = Math.floor(Math.random() * 1000)
ctx.delay(1000)

return {
createComment: {
id,
name: variables.input.name,
body: variables.input.body,
createdAt: new Date().toISOString(),
},
}
})

return <CommentForm />
}

export default { title: 'Components/CommentForm' }
info

それでもエラーが発生する場合は、ブラウザでStorybookのタブを再読み込みしてみてください。

mockGraphQLMutation を使用するには、インターセプトしたいミューテーションの名前を指定して呼び出し、インターセプトを処理し応答を返す関数を指定します。その関数に渡される引数によって、レスポンスをどのように処理するかについて柔軟性を持たせることができます。

この場合、ミューテーションに渡された変数( namebody ) とコンテキストオブジェクト(略して ctx ) が必要で、サーバへのラウンドトリップ(処理の行き来)をシミュレートするために遅延を追加することができます。これにより Submit ボタンが1秒間無効になり、最初のコメントの保存が終わるまでは2番目のコメントを投稿できないことをテストすることができます。

今すぐフォームを試してみると、エラーはなくなっているはずです。また Submitボタンが視覚的に無効になり、1秒間の遅延の間にクリックしても何も起こらないはずです。

Adding the Form to the Blog Post

ブログ記事の既存コメントの真上は、おそらく私たちのフォームが置かれるべき場所です。では、Article コンポーネントに CommentsCell コンポーネントと一緒に追加するべきなのでしょうか?もしコメントの一覧を表示するところに新しいコメントを追加するフォームも含めるのであれば、CommentsCell コンポーネント自体に追加した方が良いように感じます。しかし、これには問題があります:

もし CommentsCellSuccess コンポーネントに CommentForm を配置したら、まだコメントがないときはどうなるでしょうか?フォームが含まれていない Empty コンポーネントがレンダリングされます!そのため、最初のコメントを追加することができなくなります。

CommentFormEmpty コンポーネントにコピーすることもできますが、このようにコードが重複していることは、気づいたらすぐにデザインについて考え直すタイミングであるという兆候です。

おそらく CommentsCell は本当にコメントを取得して表示することだけを責務として担うべきでしょう。ユーザの入力も受け付けるというのは、主要な関心事から外れているように思います。

そこで、これらすべてのばらばらのパーツが組み合わされる掃除屋として Article を使ってみましょう。 -- 実際のブログ記事、新しいコメントを追加するフォーム、コメントのリスト(とそれらの間の小さなマージン)です:

web/src/components/Article/Article.js
import { Link, routes } from '@redwoodjs/router'

import CommentForm from 'src/components/CommentForm'
import CommentsCell from 'src/components/CommentsCell'

const truncate = (text, length) => {
return text.substring(0, length) + '...'
}

const Article = ({ article, summary = false }) => {
return (
<article>
<header>
<h2 className="text-xl text-blue-700 font-semibold">
<Link to={routes.article({ id: article.id })}>{article.title}</Link>
</h2>
</header>
<div className="mt-2 text-gray-900 font-light">
{summary ? truncate(article.body, 100) : article.body}
</div>
{!summary && (
<div className="mt-12">
<CommentForm />
<div className="mt-12">
<CommentsCell />
</div>
</div>
)}
</article>
)
}

export default Article

image

Storybookではいい感じです。実際のサイトではどうでしょうか?

image

いよいよ、究極のテストです:コメント作成!さあ、やってみましょう:

image

何が起こったのでしょうか?エラーメッセージの最後の方に注目してください: Field "postId" of required type "Int!" was not provided (必須型 "Int!" のフィールド "postId" が提供されていません)。データスキーマを作成したときに、ブログ記事は postId フィールドを介してコメントに所属すると説明しました。そしてその postId フィールドは必須なので、GraphQLサーバはこのフィールドを含んでいないリクエストを拒否しています。私たちは namebody だけを送信しています。幸い Article に渡される article オブジェクトのおかげで、コメントしているブログ記事の ID にアクセスすることができます!

Why didn't the Storybook story we wrote earlier expose this problem?

ストーリーの中でGraphQLのレスポンスを手動でモックしたので、私たちのモックは入力に関係なく常に正しいレスポンスを返してくれます!

モックデータを作成する際には、常にトレードオフがあります -- テストをとてもシンプルにするとGraphQLスタック全体に依存する必要がなくなるため、できるだけ現実に即したテストをしようと思ったら 現実に起こることをコードで書き直す ことになります。この場合 postId を省略するのは1回限りの修正なので、省略した場合にどうなるかをシミュレートするストーリー/モック/テストを作成する作業を行う価値はないでしょう。

しかし、もし CommentForm がアプリケーション全体で再利用されるコンポーネントになった場合、あるいは他の開発者が常に変更を加えるためコード自体が大きく変化する場合、インターフェイス (渡される props と期待される戻り値) が正確にあなたの望むものになるように時間を投資する価値はあるかもしれません。

まず、ブログ記事の ID を CommentForm に props として渡します:

web/src/components/Article/Article.js
import { Link, routes } from '@redwoodjs/router'
import CommentsCell from 'src/components/CommentsCell'
import CommentForm from 'src/components/CommentForm'

const truncate = (text, length) => {
return text.substring(0, length) + '...'
}

const Article = ({ article, summary = false }) => {
return (
<article>
<header>
<h2 className="text-xl text-blue-700 font-semibold">
<Link to={routes.article({ id: article.id })}>{article.title}</Link>
</h2>
</header>
<div className="mt-2 text-gray-900 font-light">
{summary ? truncate(article.body, 100) : article.body}
</div>
{!summary && (
<div className="mt-12">
<CommentForm postId={article.id} />
<div className="mt-12">
<CommentsCell />
</div>
</div>
)}
</article>
)
}

export default Article

そして、その ID を CommentFormcreateComment に渡される input オブジェクトに追加します:

web/src/components/CommentForm/CommentForm.js
const CommentForm = ({ postId }) => {
const [createComment, { loading, error }] = useMutation(CREATE)

const onSubmit = (input) => {
createComment({ variables: { input: { postId, ...input } } })
}

return (
//...
)
}

では、コメントフォームに必要事項を記入して送信してください そうすると...何も起こりませんでした!信じられないかもしれませんが、実はこれは状況が改善されたのです -- もうこれでエラーはありません! ページを再読み込みしてみるとどうでしょう?

image

やったー!コメントを送信すると同時に表示されればもっと良かったので、5合目って感じでしょうか?しかし、テキストボックスに私たちの名前とメッセージが入力されたまま(ページを再読み込みする前の状態)なのは、理想的ではありません。でも、どちらも修正できます。一つはGraphQLクライアント(Apollo)に新しいレコードを作成したことを伝え、できれば、このページのコメントを取得するクエリをもう一度試してもらうことです。もう一つは、新しいコメントが送信されたときに、ページからフォームを完全に削除します。

GraphQL Query Caching

Apollocaching における complexities (複雑さ)についてはたくさん書かれていますが、簡潔にするために(そして健全にするために)、最も簡単に機能することを行うつもりで、それはセルにおいてApolloにクエリを再実行してコメントを表示することを伝えます。これは "refetching" として知られています。

ミューテーション関数(ここでは createComment )に渡す変数と一緒に、 refetchQueries というオプションがあります。 これは再実行する必要があるクエリの配列で、これはおそらく、いま変更したデータがクエリ結果に反映されるからです。 この例ではひとつのクエリ、CommentsCell でエクスポートした QUERY があります。 これを CommentForm の先頭でインポートし(そして名前を変更し、他のコードから見てもそれが何であるかがわかるようにします)、 refetchQueries オプションに渡します:

web/src/components/CommentForm/CommentForm.js
import {
Form,
FormError,
Label,
TextField,
TextAreaField,
Submit,
} from '@redwoodjs/forms'
import { useMutation } from '@redwoodjs/web'

import { QUERY as CommentsQuery } from 'src/components/CommentsCell'

// ...

const CommentForm = ({ postId }) => {
const [createComment, { loading, error }] = useMutation(CREATE, {
refetchQueries: [{ query: CommentsQuery }],
})

//...
}

これで、コメントを作成すると、すぐに表示されるようになりました!作成したコメントはコメントリストの一番下にあるので、わかりにくいかもしれません(コメントを古いものから新しいものへと時系列に読みたい場合は、この位置が適しています)。ページの最後に追加されたことに気づかないユーザのために、コメントが成功したことを知らせる小さな通知をポップアップしてみましょう。

古き良きReactのstateを利用して、コメントがフォームに投稿されたかどうかを追跡してみます。投稿されたら、コメントフォームを完全に削除して、 "Thanks for your comment" というメッセージを表示させましょう。Redwoodにはポップアップ通知を表示するための react-hot-toast が含まれているので、それを使ってユーザのコメントにお礼を言いましょう。CSSのクラスをいくつか指定するだけで、フォームを削除することができます:

web/src/components/CommentForm/CommentForm.js
import { useState } from 'react'

import {
Form,
FormError,
Label,
TextField,
TextAreaField,
Submit,
} from '@redwoodjs/forms'
import { useMutation } from '@redwoodjs/web'
import { toast } from '@redwoodjs/web/toast'

import { QUERY as CommentsQuery } from 'src/components/CommentsCell'

const CREATE = gql`
mutation CreateCommentMutation($input: CreateCommentInput!) {
createComment(input: $input) {
id
name
body
createdAt
}
}
`

const CommentForm = ({ postId }) => {
const [hasPosted, setHasPosted] = useState(false)
const [createComment, { loading, error }] = useMutation(CREATE, {
onCompleted: () => {
setHasPosted(true)
toast.success('Thank you for your comment!')
},
refetchQueries: [{ query: CommentsQuery }],
})

const onSubmit = (input) => {
createComment({ variables: { input: { postId, ...input } } })
}

return (
<div className={hasPosted ? 'hidden' : ''}>
<h3 className="font-light text-lg text-gray-600">Leave a Comment</h3>
<Form className="mt-4 w-full" onSubmit={onSubmit}>
<FormError
error={error}
titleClassName="font-semibold"
wrapperClassName="bg-red-100 text-red-900 text-sm p-3 rounded"
/>
<Label
name="name"
className="block text-xs font-semibold text-gray-500 uppercase"
>
Name
</Label>
<TextField
name="name"
className="block w-full p-1 border rounded text-sm "
validation={{ required: true }}
/>

<Label
name="body"
className="block mt-4 text-xs font-semibold text-gray-500 uppercase"
>
Comment
</Label>
<TextAreaField
name="body"
className="block w-full p-1 border rounded h-24 text-sm"
validation={{ required: true }}
/>

<Submit
disabled={loading}
className="block mt-4 bg-blue-500 text-white text-xs font-semibold uppercase tracking-wide rounded px-3 py-2 disabled:opacity-50"
>
Submit
</Submit>
</Form>
</div>
)
}

export default CommentForm

image

フォームと "Leave a comment" のタイトルを完全に隠すために hidden を使いましたが、コンポーネント自体はマウントしたままです。しかし "Thank you for your comment" の通知はどこにあるのでしょうか?メッセージを表示するためには、(react-hot-toastの) Toaster コンポーネントをアプリ内のどこかに追加する必要があります。CommentForm に追加することはできますが、しかし CommentForm がマウントされていないときでも他のコードで通知したい場合はどうしたらよいでしょうか?どこにでも表示されるべき UI 要素をどこに置けばいいのでしょうか?それは BlogLayout です!

web/src/layouts/BlogLayout/BlogLayout.js
import { Link, routes } from '@redwoodjs/router'
import { Toaster } from '@redwoodjs/web/toast'

import { useAuth } from 'src/auth'

const BlogLayout = ({ children }) => {
const { logOut, isAuthenticated, currentUser } = useAuth()

return (
<>
<Toaster />
<header className="relative flex justify-between items-center py-4 px-8 bg-blue-700 text-white">
<h1 className="text-5xl font-semibold tracking-tight">
<Link
className="text-blue-400 hover:text-blue-100 transition duration-100"
to={routes.home()}
>
Redwood Blog
</Link>
</h1>
<nav>
<ul className="relative flex items-center font-light">
<li>
<Link
className="py-2 px-4 hover:bg-blue-600 transition duration-100 rounded"
to={routes.about()}
>
About
</Link>
</li>
<li>
<Link
className="py-2 px-4 hover:bg-blue-600 transition duration-100 rounded"
to={routes.contact()}
>
Contact
</Link>
</li>
<li>
{isAuthenticated ? (
<div>
<button type="button" onClick={logOut} className="py-2 px-4">
Logout
</button>
</div>
) : (
<Link to={routes.login()} className="py-2 px-4">
Login
</Link>
)}
</li>
</ul>
{isAuthenticated && (
<div className="absolute bottom-1 right-0 mr-12 text-xs text-blue-300">
{currentUser.email}
</div>
)}
</nav>
</header>
<main className="max-w-4xl mx-auto p-12 bg-white shadow rounded-b">
{children}
</main>
</>
)
}

export default BlogLayout

ここでコメントを追加します:

image

Almost Done?

ということで、ここまでで一応終了のようです!トップページに戻って、別のブログ記事を見てみてください。 私たちの素晴らしいコーディング能力の栄誉に酔いしれましょう、そして -- OH-NO:

image

全てのブログ記事に同じコメントがついています! 私たちは何をしたのでしょう??

数ページ前の、 comments() サービスは すべての コメントを返すだけだが、という伏線を覚えていますか? それがついに回収されました:あるブログ記事のコメントを取得したとき、実はその投稿だけのコメントを取得しているわけではありません。つまり、 postId を完全に無視して、データベースにある すべての コメントを返しているのです!古来からの原則が正しいと証明されました:コンピュータは指示したとおりにしか動かないのです。

直しましょう!

Returning Only Some Comments

一部のコメントだけを表示させるためには、フロントエンドとバックエンドの両方を変更する必要があります。バックエンドから始めて、この変更を行うために少しテスト駆動開発をしてみましょう。

Introducing the Redwood Console

Prismaの呼び出しにいくつかの引数を送信して、1つのブログ記事のコメントを要求できることを確認できれば、それが動作するかどうかを確認するためにアプリにすべてのスタック(コンポーネント/セル、GraphQL、サービス)を記述する必要はありません。

そこで、Redwood Consoleの登場です!新しいターミナルインスタンスで、次のようにしてみてください:

yarn rw console

標準的な Node コンソールが表示されますが、Redwood の内部のほとんどがすでにインポートされており、すぐに使えるようになっています!最も重要なのは、データベースがすぐに使えることです。試してみてください:

> db.comment.findMany()
[
{
id: 1,
name: 'Rob',
body: 'The first real comment!',
postId: 1,
createdAt: 2020-12-08T23:45:10.641Z
},
{
id: 2,
name: 'Tom',
body: 'Here is another comment',
postId: 1,
createdAt: 2020-12-08T23:46:10.641Z
}
]

(もちろん、すでにデータベースにあるコメントの内容によって、出力は若干異なります)

与えられた postId のコメントのみを取得する構文を試してみましょう:

> db.comment.findMany({ where: { postId: 1 }})
[
{
id: 1,
name: 'Rob',
body: 'The first real comment!',
postId: 1,
createdAt: 2020-12-08T23:45:10.641Z
},
{
id: 2,
name: 'Tom',
body: 'Here is another comment',
postId: 1,
createdAt: 2020-12-08T23:46:10.641Z
}
]

さて、うまくいきましたが、リストはまったく同じです。それは、1つのブログ記事に対してしかコメントを追加していないからです!2番目のブログ記事に対するコメントを作成し、特定の postId に対するコメントのみが返されるようにしましょう。

別のブログ記事の id が必要です。ブログ記事が少なくとも2つあることを確認してください(必要であれば、管理画面から作成してください)。既存の全ブログ記事のリストを取得して、idをコピーすればよいです:

> db.post.findMany({ select: { id: true } })
[ { id: 1 }, { id: 2 }, { id: 3 } ]

よし。ではその2つ目の投稿に対して、コンソールからコメントを作成してみましょう:

> db.comment.create({ data: { name: 'Peter', body: 'I also like leaving comments', postId: 2 } })
{
id: 3,
name: 'Peter',
body: 'I also like leaving comments',
postId: 2,
createdAt: 2020-12-08T23:47:10.641Z
}

ここで、コメントクエリをもう一度、それぞれの postId を使って試してみましょう:

> db.comment.findMany({ where: { postId: 1 }})
[
{
id: 1,
name: 'Rob',
body: 'The first real comment!',
postId: 1,
createdAt: 2020-12-08T23:45:10.641Z
},
{
id: 2,
name: 'Tom',
body: 'Here is another comment',
postId: 1,
createdAt: 2020-12-08T23:46:10.641Z
}
]

> db.comment.findMany({ where: { postId: 2 }})
[
{
id: 3,
name: 'Peter',
body: 'I also like leaving comments',
postId: 2,
createdAt: 2020-12-08T23:45:10.641Z
},

素晴らしい!さて、構文のテストが終わったので、サービスで使ってみましょう。Ctrl-Cを2回押すか、.exitと入力するとコンソールを終了することができます。

Where's the await?

db を呼び出すと Promise が返されます。通常、結果をすぐに取得するためには await する必要があります。しかし、毎回 await を書くのはかなり面倒なので、Redwood コンソールがそれをやってくれます -- Redwoodが await してくれるので、、あなたがする必要はありません!

Updating the Service

テストスイートを実行してみて(あるいは、すでに実行されている場合はターミナルウィンドウを覗いてみて)、すべてのテストがいまだに合格していることを確認してください。APIサイドの "lowest level" (最下層)はサービスなので、そこから始めましょう。

tip

コードベースについて考える一つの方法は、 "top to bottom" (上から下へ)という見方です。一番上はユーザに "最も近く" ユーザが操作するもの(Reactコンポーネント)、一番下はユーザから "最も遠い" もの、ウェブアプリケーションの場合、通常はデータベースやその他のデータストア(おそらくサードパーティAPIの後ろ)だと考えられます。データベースの1つ上の階層はサービスで、これはデータベースと直接通信します。

   Browser
|
React ─┐
| │
Graph QL ├─ Redwood
| │
Services ─┘
|
Database

ここに厳密なルールはありませんが、一般的にビジネスロジック(データの移動や操作を行うコード)を下に置くほど、アプリケーションの構築と保守が容易になります。Redwoodでは、ビジネスロジックをサービスに置くことを推奨しています。サービスに置くことで、データに "最も近く" 、GraphQLインターフェースの背後にあるためです。

comments サービスのテストを開き、コンソールでテストしたように comments() 関数に postId 引数を渡すよう更新してみましょう:

api/src/services/comments/comments.test.js
scenario('returns all comments', async (scenario) => {
const result = await comments({ postId: scenario.comment.jane.postId })
expect(result.length).toEqual(Object.keys(scenario.comment).length)
})

テストスイートが実行されても、すべて合格します。JavaScriptは、あなたが突然引数を渡しても気にしません(Typescriptを使用していた場合は、この時点で実際にエラーが発生します!)。TDDでは一般的に、コードを書く前にテストが失敗するように仕向けてから、テストに合格するようなコードをテスト対象に追加します。いくつか のコメントだけを返すようになったら、このテストでは何が変わるでしょうか? 返されるコメントの数はどうでしょうか?

今回使用するシナリオを見てみましょう(デフォルトでは standard() であることを思い出してください)。

api/src/services/comments/comments.scenarios.js
export const standard = defineScenario({
comment: {
jane: {
data: {
name: 'Jane Doe',
body: 'I like trees',
post: {
create: {
title: 'Redwood Leaves',
body: 'The quick brown fox jumped over the lazy dog.',
},
},
},
},
john: {
data: {
name: 'John Doe',
body: 'Hug a tree today',
post: {
create: {
title: 'Root Systems',
body: 'The five boxing wizards jump quickly.',
},
},
},
},
},
})

ここでは各シナリオがそれぞれのブログ記事に関連付けられているので、データベース内のすべてのコメントをカウントするのではなく(今のテストがそうであるように)、コメントを取得している単一のブログ記事に付けられたコメントの数だけをカウントしましょう(今は comments() の呼び出しに postId を渡しています)。テストフォームでどのように見えるか見てみましょう:

api/src/services/comments/comments.test.js
import { comments, createComment } from './comments'
import { db } from 'src/lib/db'

describe('comments', () => {
scenario('returns all comments', async (scenario) => {
const result = await comments({ postId: scenario.comment.jane.postId })
const post = await db.post.findUnique({
where: { id: scenario.comment.jane.postId },
include: { comments: true },
})
expect(result.length).toEqual(post.comments.length)
})

// ...
})

つまり、まずサービスから、与えられた postId に対するすべてのコメントを取得します。次に、データベースから 実際の ブログ記事とコメントを取得します。そして、サービスから返されたコメントの数が、データベースで実際にブログ記事につけられたコメントの数と同じであることを期待します。いまのところこのテストは失敗し、理由が出力されます:

 FAIL   api  api/src/services/comments/comments.test.js
• comments › returns all comments

expect(received).toEqual(expected) // deep equality

Expected: 1
Received: 2

post.comments.lengthから)1を受け取ることを期待しましたが、実際には( result.length から)2を受け取りました。

再びテストする前に、実際にテストしている内容を反映させるために、テストの名前も変更しましょう:

api/src/services/comments/comments.test.js
scenario(
'returns all comments for a single post from the database',
async (scenario) => {
const result = await comments({ postId: scenario.comment.jane.postId })
const post = await db.post.findUnique({
where: { id: scenario.comment.jane.postId },
include: { comments: true },
})
expect(result.length).toEqual(post.comments.length)
}
)

では、実際の comments.js サービスを開いて、引数として postId を受け取り、それを findMany() のオプションとして使うように更新します(使っていないほうの comment() 関数ではなく、必ず [s] が付いた comments() 関数を更新してください):

api/src/services/comments/comments.js
export const comments = ({ postId }) => {
return db.comment.findMany({ where: { postId } })
}

これを保存すれば、テストは再び合格するはずです!

Updating GraphQL

次に、 comments クエリに渡す postId が必須であることを GraphQL に知らせる必要があります(現在、すべてのコメントをどこでも見ることができるビューはないので、必須にすることができます)。 comments.sdl.ts ファイルを開いてください:

api/src/graphql/comments.sdl.js
type Query {
comments(postId: Int!): [Comment!]! @skipAuth
}

ここで、開発者モードで実際のサイトを更新してみると、コメントが表示されるはずの場所にエラーが表示されます:

image

セキュリティ上の理由から、ここでは内部エラーメッセージを表示しませんが、 yarn rw dev を実行しているターミナルウィンドウをチェックすると、本当のメッセージを見ることができます:

Field "comments" argument "postId" of type "Int!" is required, but it was not provided.

そう、postId が存在しないことに文句を言っているのです -- まさに我々が望んでいることです!

これでバックエンドの更新は完了です。あとは作成する GraphQL クエリに postId を渡すよう CommentsCell に指示するだけです。

Updating the Cell

まず、セル自体に postId を取得する必要があります。CommentForm コンポーネントに postId props を追加して、新しいコメントをどのブログ記事につけるのか、わかるようにしたことを思い出してください。同じことを CommentsCell でもしてみましょう。

Article を開いてください:

web/src/components/Article/Article.js
const Article = ({ article, summary = false }) => {
return (
<article>
<header>
<h2 className="text-xl text-blue-700 font-semibold">
<Link to={routes.article({ id: article.id })}>{article.title}</Link>
</h2>
</header>
<div className="mt-2 text-gray-900 font-light">
{summary ? truncate(article.body, 100) : article.body}
</div>
{!summary && (
<div className="mt-12">
<CommentForm postId={article.id} />
<div className="mt-12">
<CommentsCell postId={article.id} />
</div>
</div>
)}
</article>
)
}

そして最後に、その postId をセル内の QUERY に渡す必要があります:

web/src/components/CommentsCell/CommentsCell.js
export const QUERY = gql`
query CommentsQuery($postId: Int!) {
comments(postId: $postId) {
id
name
body
createdAt
}
}
`

この魔法のような $postId はどこから来るのでしょうか?コンポーネントを呼び出したときに props として渡したので、Redwoodは自動的にこれを提供してくれるのです!

いくつかの異なるブログ記事にアクセスしてみると、適切なブログ記事(コンソールで作成したものを含め)に関連するコメントだけが表示されるはずです。それぞれのブログ記事にそれぞれコメントを追加すれば、適切なブログ記事につくようになります:

image

しかし、コメントを投稿してもすぐには表示されなくなったことにお気づきでしょうか!残念です!さて、もう一つやらなければならないことがあります。コメント作成ロジックに refetchQueries を指定したのを覚えていますか?適切なものを再取得するために、最初に存在した変数を含める必要があります。

Updating the Form Refetch

よし。これが最後の修正です。約束します!

web/src/components/CommentForm/CommentForm.js
const [createComment, { loading, error }] = useMutation(CREATE, {
onCompleted: () => {
setHasPosted(true)
toast.success('Thank you for your comment!')
},
refetchQueries: [{ query: CommentsQuery, variables: { postId } }],
})

さあ、コメントエンジンの完成です!私たちのブログは完全に完璧で、より良くするためにできることは全く何もありません。

それとも、何かあります?