Multiple Comments
Our amazing blog posts will obviously garner a huge and passionate fanbase and we will very rarely have only a single comment. Let's work on displaying a list of comments.
Let's think about where our comments are being displayed. Probably not on the homepage, since that only shows a summary of each post. A user would need to go to the full page to show the comments for that blog post. But that page is only fetching the data for the single blog post itself, nothing else. We'll need to get the comments and since we'll be fetching and displaying them, that sounds like a job for a Cell.
Couldn't the query for the blog post page also fetch the comments?
Yes, it could! But the idea behind Cells is to make components even more composable by having them be responsible for their own data fetching and display. If we rely on a blog post to fetch the comments then the new Comments component we're about to create now requires something else to fetch the comments and pass them in. If we re-use the Comments component somewhere, now we're fetching comments in two different places.
But what about the Comment component we just made, why doesn't that fetch its own data?
There aren't any instances I (the author) could think of where we would ever want to display only a single comment in isolation—it would always be a list of all comments on a post. If displaying a single comment was common for your use case then it could definitely be converted to a CommentCell and have it responsible for pulling the data for that single comment itself. But keep in mind that if you have 50 comments on a blog post, that's now 50 GraphQL calls that need to go out, one for each comment. There's always a trade-off!
Then why make a standalone Comment component at all? Why not just do all the display in the CommentsCell?
We're trying to start in small chunks to make the tutorial more digestible for a new audience so we're starting simple and getting more complex as we go. But it also just feels nice to build up a UI from these smaller chunks that are easier to reason about and keep separate in your head.
But what about—
Look, we gotta end this sidebar and get back to building this thing. You can ask more questions later, promise!
#
StorybookLet's generate a CommentsCell:
Storybook updates with a new CommentsCell under the Cells folder, and it's actually showing something:
Where did that come from? Check out CommentsCell.mock.js
: there's no Prisma model for a Comment yet, so Redwood took a guess that your model would at least contain an id
field and just used that for the mock data.
Let's update the Success
component to use the Comment
component created earlier, and add all of the fields we'll need for the Comment to render to the QUERY
:
We're passing an additional key
prop to make React happy when iterating over an array with map
.
If you check Storybook, you'll see that we do indeed render the Comment
component three times, but there's no data to display. Let's update the mock with some sample data:
What's this
standard
thing? Think of it as the standard, default mock if you don't do anything else. We would have loved to use the name "default" but that's already a reserved word in Javascript!
Storybook refreshes and we've got comments! It's a little hard to distinguish between the two separate comments because they're right next to each other:
Since CommentsCell
is the one responsible for drawing multiple comments, it makes sense that it should be "in charge" of how they're displayed, including the gap between them. Let's add a style to do that in CommentsCell
:
space-y-8
is a handy Tailwind class that puts a space between elements, but not above or below the entire stack (which is what would happen if you gave each<Comment>
its own top/bottom margin).
Looking good! Let's add our CommentsCell to the actual blog post display page:
If we are not showing the summary, then we'll show the comments. Take a look at the Full and Summary stories in Storybook and you should see comments on one and not on the other.
Shouldn't the CommentsCell cause an actual GraphQL request? How does this work?
Redwood has added some functionality around Storybook so that if you're testing a component that itself isn't a Cell (like the
Article
component) but that renders a cell (likeCommentsCell
), then it will mock the GraphQL and use thestandard
mock that goes along with that Cell. Pretty cool, huh?
Adding the comments to the article display has exposed another design issue: the comments are sitting right up underneath the article text:
Let's add a gap between the two:
Okay, comment display is looking good! However, you may have noticed that if you tried going to the actual site there's an error where the comments should be:
Why is that? Remember that we started with the CommentsCell
, but never actually created a Comment model in schema.prisma
or created an SDL and service! We'll be rectifying this soon. But this demonstrates another huge benefit of working with Storybook: you can build out UI functionality completely isolated from the api-side. In a team setting this is great because a web-side team can work on the UI while the api-side team can be building the backend end simultaneously and one doesn't have to wait for the other.
#
TestingWe added a component, CommentsCell
, and edited another, Article
, so what do we test, and where?
#
Testing CommentsThe actual Comment
component does most of the work so there's no need to test all of that functionality again in CommentsCell
: our Comment
tests cover that just fine. What things does CommentsCell
do that make it unique?
- Has a loading message
- Has an error message
- Has a failure message
- When it renders successfully, it outputs as many comments as were returned by the
QUERY
(what is rendered we'll leave to theComment
tests)
The default CommentsCell.test.js
actually tests every state for us, albeit at an absolute minimum level—it make sure no errors are thrown:
And that's nothing to scoff at! As you've probably experienced, a React component usually either works 100% or blows up spectacularly. If it works, great! If it fails then the test fails too, which is exactly what we want to happen.
But in this case we can do a little more to make sure CommentsCell
is doing what we expect. Let's update the Success
test in CommentsCell.test.js
to check that exactly the number of comments we passed in as a prop are rendered. How do we know a comment was rendered? How about if we check that each comment.body
(the most important part of the comment) is present on the screen:
We're looping through each comment
from the mock, the same mock used by Storybook, so that even if we add more later, we're covered. You may find youself writing a test and saying "just test that there are 3 comments," which will work today, but months from now when you add more comments to the mock to try some different iterations in Storybook, that test will start failing. Avoid hardcoding data like this into your test when you can derive it from your mocked data!
#
Testing ArticleThe functionality we added to Article
says to show the comments for the post if we are not showing the summary. We've got a test for both the "full" and "summary" renders already. Generally you want your tests to be testing "one thing" so let's add two additional tests for our new functionality:
Notice we're importing the mock from a completely different component—nothing wrong with that!
We're introducing a new test function here, waitFor()
, which will wait for things like GraphQL queries to finish running before checking for what's been rendered. Since Article
renders CommentsCell
we need to wait for the Success
component of CommentsCell
to be rendered.
The summary version of
Article
does not render theCommentsCell
, but we should still wait. Why? If we did mistakenly start includingCommentsCell
, but didn't wait for the render, we would get a falsely passing test—indeed the text isn't on the page but that's because it's still showing theLoading
component! If we had waited we would have seen the actual comment body get rendered, and the test would (correctly) fail.
Okay we're finally ready to let users create their comments.