What do you want to see?

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.

Published by

Kevin

I've been coding professionally for 30 years, 25 of those in Silicon Valley. I'm home now in beautiful Bristol, England.

Leave a Reply

Your email address will not be published. Required fields are marked *