Skip to main content
Version: 5.3

Multiple Comments

私たちの素晴らしいブログ記事は、すさまじい数の情熱的なファンを集め、コメントが1つしかないことはほとんどないでしょう。コメントの一覧を表示させましょう。

私たちのコメントがどこに表示されるかを考えてみましょう。おそらくホームページではないでしょう。それぞれのブログ記事のサマリしか表示されないですからね。ユーザは、そのブログ記事のコメントを表示するために、フルページに移動する必要があるでしょう。しかし、このページは単一のブログ記事自体のデータを取得するだけで、他には何もありません。コメントを取得し かつ 表示する必要があるので、これはセルのお仕事のようですね。

Couldn't the query for the blog post page also fetch the comments?

はい、できます!しかし、セルの背後にあるアイデアは、コンポーネントが自身のデータフェッチ 表示の責務を担うことで、コンポーネントをさらにcomposableにすることです。コメントの取得がブログ記事に依存するとしたら、これから作成する新しいCommentsコンポーネントは、コメントを取得して渡すために コメントとは別の何か が必要になります。Commentsコンポーネントをどこかで再利用すると、今度は2つの異なる場所でコメントを取得することになります。

しかし、先ほど作ったCommentコンポーネントはどうでしょうか、なぜそれ自身のデータを取得しないのでしょうか?

私(作者)が思いつく限りでは、単一のコメントだけを単独で表示したいようなケースはないでしょう -- それは常に投稿されたすべてのコメントのリストです。もしあなたのユースケースで単一のコメントを表示することが一般的であれば、それは間違いなく CommentCell に書き換えて、その単一のコメント自体のデータを取得する責務を担うべきです。しかし、ブログの記事に50のコメントがある場合、それぞれ個別の、計50回のGraphQLコールが必要であることに留意してください。常にトレードオフの関係にあるのです!

では、なぜスタンドアローン(独立した)のCommentコンポーネントを作るのでしょうか?すべての表示をCommentsCellで行えばよいのではないでしょうか?

私たちはチュートリアルを小さなチャンク(塊)から始めています。チュートリアルを新しい読者にとってよりわかりやすいものにするために、シンプルなものから初めて、徐々に複雑にしていきます。このような小さなチャンクからUIを構築していくのは、理屈を考えやすく、頭の中で分離しておくことができるので、とても良いことだと感じています。

でも、どうなんでしょうか --

いいですか、このサイドバーを終わらせて、これを構築に戻るのです。質問は後でどうぞ、約束です!

Storybook

CommentsCell を生成しましょう:

yarn rw g cell Comments

Cells フォルダの下に新しい CommentsCell が追加されてStorybookが更新され、何か表示されています:

image

これはどこから来たのでしょうか? CommentsCell.mock.ts をチェックしてみてください:コメント用のPrismaモデルはまだないので、Redwoodはあなたのモデルが少なくとも id フィールドを持つと推測して、それをモックデータに使用しました。

先ほど作成した Comment コンポーネントを使うよう Success コンポーネントを更新し、 Comment を描画するのに必要なフィールドすべてを QUERY に追加しましょう:

web/src/components/CommentsCell/CommentsCell.js
import Comment from 'src/components/Comment'

export const QUERY = gql`
query CommentsQuery {
comments {
id
name
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 = ({ comments }) => {
return (
<>
{comments.map((comment) => (
<Comment key={comment.id} comment={comment} />
))}
</>
)
}

Reactが配列を map で反復処理して幸せになるために、追加で key propsを渡します。

Storybookを確認すると、確かに Comment コンポーネントを3回レンダリングしていますが、表示するデータがありません。サンプルデータでモックを更新してみましょう:

web/src/components/CommentsCell/CommentsCell.mock.js
export const standard = () => ({
comments: [
{
id: 1,
name: 'Rob Cameron',
body: 'First comment',
createdAt: '2020-01-02T12:34:56Z',
},
{
id: 2,
name: 'David Price',
body: 'Second comment',
createdAt: '2020-02-03T23:00:00Z',
},
],
})
What's this standard thing?

これは、何もしない場合の標準的な、デフォルトのモックだと考えてください。私たちは "default" という名前を使いたかったのですが、これはすでにJavaScriptでは予約語になっています!

ストーリーブックが更新され、コメントがつきました!2つの別々のコメントがすぐ隣り合っていて区別がつきづらいですね:

image

CommentsCell は複数のコメントを描画する責務を担っているので、コメント間の隙間含めコメントの表示方法を "担う" ことは理にかなっています。そのためのスタイルを CommentsCell に追加してみましょう:

web/src/components/CommentsCell/CommentsCell.js
export const Success = ({ comments }) => {
return (
<div className="space-y-8">
{comments.map((comment) => (
<Comment comment={comment} key={comment.id} />
))}
</div>
)
}
tip

space-y-8 は Tailwind の便利なクラスで、要素の間に between のスペースを入れますが、スタック全体の上や下には入れません(各 <Comment> に独自の上下マージンを与えると、このようになります)。

いい感じ!CommentsCellを、実際にブログ記事を表示するページに追加してみましょう:

web/src/components/Article/Article.js
import { Link, routes } from '@redwoodjs/router'
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 && <CommentsCell />}
</article>
)
}

export default Article

要約を表示 しない のであれば、コメントを表示することになります。Storybookの FullSummary のストーリーを見てみると、片方にはコメントが表示され、もう片方には表示されないはずです。

Shouldn't the CommentsCell cause an actual GraphQL request? How does this work?

RedwoodはStorybookにいくつかの機能を追加しました。( Article コンポーネントのように)セルではないものの、( CommentsCell のように)セルをレンダリングするコンポーネントをテストする場合、GraphQLをモックしてそのセルに対応した standard (標準的な)モックを使用します。かなりクールでしょう?

ブログ記事表示にコメントを追加することで、別のデザイン上の問題が露呈しました:それは、コメントがブログ記事本文のすぐ下に表示されることです:

image

本文とコメントの間に隙間を空けましょう:

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">
<CommentsCell />
</div>
)}
</article>
)
}

image

さて、コメントの表示もいい感じです!しかし、実際のサイトにアクセスしてみると、コメントが表示されるはずの場所にエラーがあることにお気づきかもしれません:

image

なぜでしょうか?私たちは CommentsCell から始めましたが、実際には schema.prisma にコメントモデルを作成したり、SDL やサービスを作成していませんでした!これはすぐに修正する予定です。しかし、これはStorybookで作業することのもう一つの大きな利点を示しています:それは、UI機能をAPIサイドから完全に分離して構築できることです。WebサイドのチームがUIに取り組み、APIサイドのチームがバックエンドエンドを並行して構築することができます。チーム開発において一方が他方を待つ必要がないのは素晴らしいことです。

Testing

コンポーネントである CommentsCell を追加し、別のコンポーネントである Article を編集しました。では何を、どこでテストすればいいのでしょうか?

Testing Comments

実際の Comment コンポーネントがほとんどの作業を行うので、 CommentsCell でその機能すべてを改めてテストする必要はありません: Comment のテストで十分にカバーできます。 CommentsCell が持つ独自の機能は何でしょうか?

  • ロードメッセージがある
  • エラーメッセージがある
  • 失敗メッセージがある
  • 描画に成功すると QUERY が返したのと同じ数のコメントを表示する( 何が 描画されるかは Comment のテストに任せる)

デフォルトの CommentsCell.test.tsx は、ごく最小限ではあるものの、実際にすべての状態をテストしてくれます -- エラーがスローされないことを確認します:

web/src/components/CommentsCell/CommentsCell.test.js
import { render } from '@redwoodjs/testing/web'

import { Loading, Empty, Failure, Success } from './CommentsCell'
import { standard } from './CommentsCell.mock'

describe('CommentsCell', () => {
it('renders Loading successfully', () => {
expect(() => {
render(<Loading />)
}).not.toThrow()
})

it('renders Empty successfully', async () => {
expect(() => {
render(<Empty />)
}).not.toThrow()
})

it('renders Failure successfully', async () => {
expect(() => {
render(<Failure error={new Error('Oh no')} />)
}).not.toThrow()
})

it('renders Success successfully', async () => {
expect(() => {
render(<Success comments={standard().comments} />)
}).not.toThrow()
})
})

そして、それは決してバカにできるものではありません!皆さんも経験があると思いますが、Reactのコンポーネントは通常、100%動作するか、あるいは見事に吹っ飛ぶかのどちらかです。もし動けば、素晴らしいことです!もし失敗したら、テストも失敗することになり、何か起きていることがわかります。

しかしこの場合、 CommentsCell が期待通りに動作していることを確認するために、もう少し工夫できます。 CommentsCell.test.tsSuccess テストを更新して、props として渡したコメントの数が正確に描画されることを確認しましょう。 コメントがレンダリングされたことを知るにはどうしたらよいでしょうか? それぞれの comment.body (コメントの最も重要な部分)が画面上に存在することを確認するのはどうでしょうか:

web/src/components/CommentsCell/CommentsCell.test.js
import { render, screen } from '@redwoodjs/testing/web'

import { Loading, Empty, Failure, Success } from './CommentsCell'
import { standard } from './CommentsCell.mock'

describe('CommentsCell', () => {
it('renders Loading successfully', () => {
expect(() => {
render(<Loading />)
}).not.toThrow()
})

it('renders Empty successfully', async () => {
expect(() => {
render(<Empty />)
}).not.toThrow()
})

it('renders Failure successfully', async () => {
expect(() => {
render(<Failure error={new Error('Oh no')} />)
}).not.toThrow()
})

it('renders Success successfully', async () => {
const comments = standard().comments
render(<Success comments={comments} />)

comments.forEach((comment) => {
expect(screen.getByText(comment.body)).toBeInTheDocument()
})
})
})

Storybookで使用しているモックと同じモックからの comment をループしているので、後で追加しても大丈夫です。 テストを書いていて "コメントが全部で2つあることをテストしてください" と言うと、今日はうまくいくかもしれませんが、数ヶ月後、Storybookでいろいろなイテレーションを試すためにモックにコメントを追加すると、そのテストは失敗し始めるでしょう。 このようなデータをハードコードするのは避けましょう。特に マジックナンバー は、モックデータから導き出せるのであれば、テストに入れるのは避けましょう!

Testing Article

私たちが Article に追加した機能は、要約を表示 しない 場合はブログ記事に対するコメントを表示します。 すでに "full" レンダーと "summary" レンダーの両方のテストがあります。 一般的にテストは、ブログ記事の本文が存在するかどうかのような "1つのこと" をテストし、コメントが表示されているかどうかは他のテストでテストしたいものです。もし、テストの記述に "and" を使っているようなら(例えば "ブログ記事とそのコメントを表示する" のように)、それはおそらく2つの別々のテストに分けるべきという良いサインでしょう。

新しい機能のために、2つのテストを追加しましょう:

web/src/components/Article/Article.test.js
import { render, screen, waitFor } from '@redwoodjs/testing'

import { standard } from 'src/components/CommentsCell/CommentsCell.mock'

import Article from './Article'

const ARTICLE = {
id: 1,
title: 'First post',
body: `Neutra tacos hot chicken prism raw denim, put a bird on it enamel pin post-ironic vape cred DIY. Street art next level umami squid. Hammock hexagon glossier 8-bit banjo. Neutra la croix mixtape echo park four loko semiotics kitsch forage chambray. Semiotics salvia selfies jianbing hella shaman. Letterpress helvetica vaporware cronut, shaman butcher YOLO poke fixie hoodie gentrify woke heirloom.`,
createdAt: new Date().toISOString(),
}

describe('Article', () => {
it('renders a blog post', () => {
render(<Article article={ARTICLE} />)

expect(screen.getByText(ARTICLE.title)).toBeInTheDocument()
expect(screen.getByText(ARTICLE.body)).toBeInTheDocument()
})

it('renders comments when displaying a full blog post', async () => {
const comment = standard().comments[0]
render(<Article article={ARTICLE} />)

await waitFor(() =>
expect(screen.getByText(comment.body)).toBeInTheDocument()
)
})

it('renders a summary of a blog post', () => {
render(<Article article={ARTICLE} summary={true} />)

expect(screen.getByText(ARTICLE.title)).toBeInTheDocument()
expect(
screen.getByText(
'Neutra tacos hot chicken prism raw denim, put a bird on it enamel pin post-ironic vape cred DIY. Str...'
)
).toBeInTheDocument()
})

it('does not render comments when displaying a summary', async () => {
const comment = standard().comments[0]
render(<Article article={ARTICLE} summary={true} />)

await waitFor(() =>
expect(screen.queryByText(comment.body)).not.toBeInTheDocument()
)
})
})

モックを全く別のコンポーネントからインポートしていることに注意してください -- 何も問題ありません!

ここでは新しいテスト関数である waitFor() を導入します。この関数は GraphQL クエリの実行が終了するのを待って、描画されたものをチェックします。 ArticleCommentsCell を描画するので、 CommentsCellSuccess コンポーネントが描画されるのを待つ必要があります。

info

要約バージョンの ArticleCommentsCell を描画しませんが、ここでも wait する必要があります。なぜでしょうか? 間違って CommentsCell を含めてしてしまい、描画を待たなかった場合、テストは偽の合格判定になります -- 確かにテキストはページ上にありませんが、それは Loading コンポーネントがまだ表示されているからです!もし wait していたら、実際のコメント本文が描画され、テストは(正しく)失敗していたでしょう。

さて、いよいよユーザ自身がコメントする準備が整いました。