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!
Let's generate a CommentsCell:
Storybook updates with a new CommentsCell under the Cells folder. Let's update the Success story to use the Comment component created earlier, and return all of the fields we'll need for the Comment to render:
We're passing an additional
key prop to make React happy when iterating over an array with
If you check Storybook, you'll seen an error. We'll need to update the
mock.js file that came along for the ride when we generated the Cell so that it returns an array instead of just a simple object with some sample data:
Storybook refreshes and we've got comments! We've got the same issue here where it's hard to see our rounded corners and also the two separate comments are hard to distinguish because they're right next to each other:
The gap between the two comments is a concern for this component, since it's responsible for drawing multiple comments and their layout. So let's fix that in CommentsCell:
We had to move the
key prop to the surrounding
<div>. We then gave each comment a top margin and removed an equal top margin from the entire container to set it back to zero.
Why a top margin and not a bottom margin? Remember when we said a component should be responsible for its own display? If you add a bottom margin, that's one component influencing the one below it (which it shouldn't care about). Adding a top margin is this component moving itself down, which means it's again responsible for its own display.
Let's add a margin around the story itself, similar to what we did in the Comment story:
mt-16? One of the fun rules of CSS is that if a parent and child both have margins, but no border or padding between them, their
margin-bottomcollapses. So even though the story container will have a margin of 8 (which equals 2rem) remember that the container for CommentsCell has a -8 margin (-2rem). Those two collapse and essentially cancel each other out to 0 top margin. Setting
mt-16sets a 4rem margin, which after subtracting 2rem leaves us with 2rem, which is what we wanted to start with!
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 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 if you're testing a component that itself isn't a Cell (like BlogPost) but that renders a cell (CommentsCell) that it needs to mock the GraphQL and use the
standardmock that goes along with that Cell. Pretty cool, huh?
Once again our component is bumping right up against the edges of the window. We've got two stories in this file and would have to manually add margins around both of them. Ugh. Luckily Storybook has a way to add styling to all stories using decorators. In the
default export at the bottom of the story you can define a
decorators key and the value is JSX that will wrap all the stories in the file automatically:
Save, and both the Full and Summary stories should have margins around them now.
For more extensive, global styling options, look into Storybook theming.
We could use a gap between the end of the blog post and the start of the comments to help separate 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! That's another neat part 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.
We added one component, Comments, and edited another, BlogPost, so we'll want to add tests in both.
The actual Comment component does most of the work so there's no need to test all of that functionality again. 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
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 throws an error. 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.
The functionality we added to
<BlogPost> 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:
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 BlogPost renders CommentsCell we need to wait for the
Success component of CommentsCell to be rendered.
The summary version of BlogPost does not render the CommentsCell, but we should still wait. Why? If we did mistakenly start including CommentsCell, 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 the Loading 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.