Story so far:
- We have a Rails back end that can serialize a blog post to JSON.
- We have a React front end that can render a blog post.
In this instalment, we want to connect the two together. Let’s do a bit of tidying first.
It sucks that our Application component is doing everything. Let’s extract a Post component. Test first.
I’ll start by copy-pasting application/Application.test.jsx to posts/Post.test.jsx and then strip out the non-post stuff.
describe('The post component', () => { test('shows a blog post', () => { const component = mount(<Post />) expect(component.find('.title').text()).toBe('React on Rails') expect(component.find('.body').text()).toBe('I can use React with Rails.') }) })
I’ll do the same thing with a Post component and check that all the tests still pass.
import React from 'react' export function Post(_props) { return <div className='post'> <h2 className='title'>React on Rails</h2> <div className='body'>I can use React with Rails.</div> </div> }
Now I’ll change Application to use the Post component and get rid of the duplication.
import React from 'react' import {Post} from 'posts/Post' export function Application(_props) { return <div id='application'> <h1 className='site-name'>Blogging</h1> <Post /> </div> }
The tests still pass. Refactoring successful. There’s some overlap between Application.test and Post.test but I’m OK with that.
We’re almost ready to hook the Post component up to the backend but there’s one more fake it step we can take on the road to making it. Let’s pass the post data into the Post component so it doesn’t need to concern itself with where its data came from. Small steps are the best steps.
I’m going to introduce a mediating component between Application and Post that will be responsible for fetching the data. I’ll use the convention ConnectedXXX for a component that connects a view to its data provider.
Here it is (the data is still hard coded for the moment).
export function Post({post}) { return <div className='post'> <h2 className='title'>{post.title}</h2> <div className='body'>{post.body}</div> </div> } export default function ConnectedPost() { const post = { id: 1, title: 'React on Rails', body: 'I can use React with Rails.', } return <Post post={post} /> }
Notice that I used the default modifier when I exported the ConnectedPost. You can export multiple components (or functions or constants or whatever) from each Javascript file but only one of them can be the default.
When you import a component from another file, you can use named imports like this:
// Application.jsx import {Post} from 'posts/Post' ... <Post />
If I skip the braces and import the default export (ConnectedPost), I can call it whatever name I like. I’m gonna continue to call it Post (because the caller doesn’t need to know about implementation details like data providers), like this:
// Application.jsx import Post from 'posts/Post' ... <Post />
I won’t show the import statements in my code snippets any more unless there is something significant that I want to draw your attention to. The basic rules to remember are:
- If you use a component from another file, you have to import it.
- Remember to indicate whether you want the default component or a named component.
- Import React in any file that uses JSX.
RubyMine takes care of imports for me magically so I am barely aware of them most of the time anyway.
Now that the post object is no longer hard-coded, we can update Post.test.jsx as a unit test for Post and pass the post in from the outside. We’ll test the ConnectedPost separately later.
const post = { id: 1, title: 'The title', body: 'The body.', } test('shows a blog post', () => { const component = mount(<Post post={post}/>) expect(component.find('.title').text()).toEqual('The title') expect(component.find('.body').text()).toEqual('The body.') })
Before I do the next interesting change I need to introduce some new concepts. Let’s start with class components.
Class components
All of our components so far have been stateless so it makes sense that they are pure functions. We’ll need to maintain state for our ConnectedPost when it retrieves data from the server so we are going to convert it to a class component.
Aside: The React folks have added hooks to allow you to maintain state in a functional component but the whole thing still feels weird to me. I’m sticking with class components for now.
export default class ConnectedPost extends React.Component { render() { const post = { id: 1, title: 'React on Rails', body: 'I can use React with Rails.', } return <Post post={post} /> } }
This class component is exactly equivalent to the functional component that it replaced with all the rendering logic moved into a method called render().
Functional components are passed their properties as parameters to the render method like this render(props). Class components access their properties through an instance variable: this.props.
Class components give us two new capabilities. The first is state management.
State Management
React gives us a this.state instance variable that lets us keep track of internal changes to a component like when the user checks a box or presses a button. In our case, we are going to keep track of the post that we are fetching from the server.
There are three aspects to state management.
- Initialize the state in the constructor.
- Access the state in the render() method (and other methods as necessary).
- Change the current state with this.setState().
The reason we are letting React manage our state rather than just using an instance variable is to give React the chance to re-render our component every time the state changes.
Let’s look at the first two aspects together. I’ll change the component to initialize the state with an empty post in the constructor and then access that post in the render() method.
export default class ConnectedPost extends React.Component { constructor(props) { super(props) this.state = { post: {} } } render() { const {post} = this.state return <Post post={post} /> } }
Before I show you how to change the state, let me introduce the second capability of class components.
Lifecycle methods
The second new capability of class components is lifecycle methods. Lifecycle methods get called when a component is initialized or when some external event happens that the component needs to know about. The most important lifecycle method is componentDidMount().
You read about other lifecycle methods in the React documentation.
Let’s use componentDidMount() now to change the state when the component is first mounted.
export default class ConnectedPost extends React.Component { ... componentDidMount() { const post = { id: 1, title: 'React on Rails', body: 'I can use React with Rails.', } this.setState({post}) } }
We’re now exactly back to where we started but now we have a place to call out to the server to fetch out post. Let’s do it.
componentDidMount() { const request = { url: '/posts/1.json' } return axios(request).then(response => { const post = response.data this.setState({post}) }) }
I’m using axios rather than fetch. My reason is lost in the mists of time but I remember that fetch did something weird with 404 errors that I found annoying. Use fetch if you prefer.
We have to install axios with Yarn…
yarn add axios
…and import it at the top of the Post.jsx file.
import * as axios from 'axios'
We are finally displaying an actual blog post that we loaded from the server but the new changes broke our tests.
We’ll fix that tomorrow.