As of now, we can create a new post and we can show one hard-coded post. It’s almost time to add some structure and navigation to our app. Let’s show a list of posts first, though, and we can think about how we navigate between items later.
Let’s add a list component to show all the posts. We’ll fake it in the front end…
export function Main() { return <div id='main'> <Editor onSubmit={create} /> <List /> </div> }
// /javascript/posts/List.jsx export default class List extends React.Component { render() { return 'list of posts' } }
… until we have the backend in place.
The posts.index
action in PostsController
will return the React application if you request HTML and a list of posts if you request JSON.
# PostsControllerTest.rb test 'the index page just returns the react application' do get posts_url assert_select '#react' end test 'fetch a list of posts as json' do get posts_url(format: :json) assert_response :success json = JSON.parse response.body, symbolize_names: true assert 1, json[:posts].count assert 'The title', json[:posts][0][:title] end
# PostsController.rb def index @posts = Post.all.order(created_at: :desc) end # index.json.jbuilder json.posts do json.array! @posts do |post| json.extract! post, :id, :title, :body, :created_at, :updated_at end end
And, if I open http://blogging.local:3000/posts.json
in the browser, I see my posts. Now to fetch them from the client.
I’ll add an API methods to fetch the list of posts…
// posts/api.js export function list() { return server.get(`/posts.json`) }
…and update the List component to call it and render the list.
export default class List extends React.Component { constructor(props) { super(props) this.state = { posts: null } } render() { const {posts} = this.state if(posts === null) return 'Loading…' return <ul className='posts-list'> { posts.map(post => <li key={post.id}> <Post post={post}/> </li>) } </ul> } async componentDidMount() { const response = await list() this.setState({posts: response.posts}) } }
That’s all straightforward, I think, except: note the key property in <li key={post.id}>.
React performs optimisations by keeping track of changes to the React component tree and only re-rendering components that have changed since the last render. When you render an array of components you should provide a unique key to each component to help React figure out what has changed.
If I run all the Javascript tests now, I see a failure in the top-level Application.test.jsx because it is still expecting a single Post component rather than a list.
Let’s fix that.
// Application.test.jsx describe('The application', () => { server.send = jest.fn() const post = { id: 1, title: 'React on Rails', body: 'I can use React with Rails.', } const posts = [post] beforeEach( () => { // Return a list of posts instead of a single post server.send.mockReturnValue({posts}) }) test('shows a list of blog posts', async () => { const component = await display(<Application />) component.update() assert_select(component, '.site-name', 'Blogging') assert_select(component, '.post .title', 'React on Rails') assert_select(component, '.post .body', 'I can use React with Rails.') expect(server.send).toBeCalledWith('/posts.json') }) })
I’m not sure why, but I had to add component.update() to get the list component to render the correct HTML (I think this might be a bug in Enzyme).
I like to think of the top-level Application.test as a kind of smoke test that tests the whole application from end to end, albeit in a rather shallow fashion. It won’t tell us anything profound about the application’s behaviour but it might flag some future regression that would be missed by lower-level unit tests.
As I did with the Post.test, I’ll duplicate this test as a unit test so I can test the List component more thoroughly. But first I am going to split the component into a List that knows how to render a list of posts and a ConnectedList that knows how to fetch a list of posts from the server.
This seems like a lot of faff when I am typing it into WordPress but it’s literally 10 seconds of copy-paste in the IDE and my future self will thank me when I need to write more complex tests and I can do so without loading the whole framework.
Here’s the code.
// posts/List.jsx export function List({posts}) { return <ul className='posts-list'> { posts.map(post => <li key={post.id}> <Post post={post}/> </li>) } </ul> } export default class ConnectedList extends React.Component { constructor(props) { super(props) this.state = { posts: null } } render() { const {posts} = this.state if(posts === null) return 'Loading…' return <List posts={posts} /> } async componentDidMount() { const response = await list() this.setState({posts: response.posts}) } }
And here are the tests: one for the List and one for the ConnectedList.
// posts/List.test.jsx describe('The post list', () => { server.send = jest.fn() const post = { id: 1, title: 'React on Rails', body: 'I can use React with Rails.', } const posts = [post] test('shows a list of blog posts', () => { const component = display(<List posts={posts}/>) assert_select(component, '.posts-list .post', 1) assert_select(component, '.post .title', 'React on Rails') assert_select(component, '.post .body', 'I can use React with Rails.') }) test('fetches the list of blog posts from the server', async () => { server.send.mockReturnValue({posts}) const component = await display(<ConnectedList />) component.update() assert_select(component, '.posts-list .post', 1) expect(server.send).toBeCalledWith('/posts.json') }) })
A quick test in the browser shows that I can show a list of posts from the server and I can create a new one except… Ooops!… the new one doesn’t get added to the list unless I refresh the page.
This is the kind of problem that Redux will solve for us but I have a different approach in mind. We’ll give it a try in the next episode.