Skip to main content
Version: 8.2

Building a Component the Redwood Way

What's our blog missing? Comments. Let's add a simple comment engine so people can leave their completely rational, well-reasoned comments on our blog posts. It's the internet, what could go wrong?

There are two main features we need to build:

  1. Comment form and creation
  2. Comment retrieval and display

Which order we build them in is up to us. To ease into things, let's start with the fetching and displaying comments first and then we'll move on to more complex work of adding a form and service to create a new comment. Of course, this is Redwood, so even forms and services aren't that complex!

Storybook

Let's create a component for the display of a single comment. First up, the generator:

yarn rw g component Comment

Storybook should refresh and our "Generated" Comment story will be ready to go:

image

Let's think about what we want to ask users for and then display in a comment. How about just their name and the content of the comment itself? And we'll throw in the date/time it was created. Let's update the Comment component to accept a comment object with those three properties:

web/src/components/Comment/Comment.jsx
const Comment = ({ comment }) => {
return (
<div>
<h2>{comment.name}</h2>
<time dateTime={comment.createdAt}>{comment.createdAt}</time>
<p>{comment.body}</p>
</div>
)
}

export default Comment

Once you save that file and Storybook reloads you'll see it blow up:

image

We need to update the story to include that comment object and pass it as a prop:

web/src/components/Comment/Comment.stories.jsx
import Comment from './Comment'

export const generated = () => {
return (
<Comment
comment={{
name: 'Rob Cameron',
body: 'This is the first comment!',
createdAt: '2020-01-01T12:34:56Z'
}}
/>
)
}

export default {
title: 'Components/Comment',
component: Comment,
}
info

Datetimes will come from GraphQL in ISO8601 format so we need to return one in that format here.

Storybook will reload and be much happier:

image

Let's add a little bit of styling and date conversion to get this Comment component looking like a nice, completed design element:

web/src/components/Comment/Comment.jsx
const formattedDate = (datetime) => {
const parsedDate = new Date(datetime)
const month = parsedDate.toLocaleString('default', { month: 'long' })
return `${parsedDate.getDate()} ${month} ${parsedDate.getFullYear()}`
}

const Comment = ({ comment }) => {
return (
<div className="bg-gray-200 p-8 rounded-lg">
<header className="flex justify-between">
<h2 className="font-semibold text-gray-700">{comment.name}</h2>
<time className="text-xs text-gray-500" dateTime={comment.createdAt}>
{formattedDate(comment.createdAt)}
</time>
</header>
<p className="text-sm mt-2">{comment.body}</p>
</div>
)
}

export default Comment

image

Our component looks great! Now let's verify that it does what we want it to do with a test.

Testing

We don't want Santa to skip our house so let's test our Comment component. We could test that the author's name and the body of the comment appear, as well as the date it was posted.

The default test that comes with a generated component just makes sure that no errors are thrown, which is the least we could ask of our components!

Let's add a sample comment to the test and check that the various parts are being rendered:

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

import Comment from './Comment'

describe('Comment', () => {
it('renders successfully', () => {
const comment = {
name: 'John Doe',
body: 'This is my comment',
createdAt: '2020-01-02T12:34:56Z',
}
render(<Comment comment={comment} />)

expect(screen.getByText(comment.name)).toBeInTheDocument()
expect(screen.getByText(comment.body)).toBeInTheDocument()
const dateExpect = screen.getByText('2 January 2020')
expect(dateExpect).toBeInTheDocument()
expect(dateExpect.nodeName).toEqual('TIME')
expect(dateExpect).toHaveAttribute('datetime', comment.createdAt)
})
})

Here we're testing for both elements of the output createdAt timestamp: the actual text that's output (similar to how we tested for an article's truncated body) but also that the element that wraps that text is a <time> tag and that it contains a datetime attribute with the raw value of comment.createdAt. This might seem like overkill but the point of the datetime attribute is to provide a machine-readable timestamp that the browser could (theoretically) hook into and do stuff with. This makes sure that we preserve that ability.

If your tests aren't already running in another terminal window, you can start them now:

yarn rw test
What happens if we change the formatted output of the timestamp? Wouldn't we have to change the test?

Yes, just like we'd have to change the truncation text if we changed the length of the truncation. One alternative approach to testing for the formatted output could be to move the date formatting formula into a function that you can export from the Comment component. Then you can import that in your test and use it to check the formatted output. Now if you change the formula the test keeps passing because it's sharing the function with Comment.