What is a render prop?
Render prop is a pattern that is widely used in React ecosystem. In a nutshell, render prop is a pattern in which you are passing a function as a prop, usually called render
or more commonly as a children
prop. For example:
import React from 'react';
const RenderPropComponent = ({children}) => {
const [counter, setCounter] = React.useState(0)
return children({counter, setCounter});
};
// usage
const Usage = () => {
return (
<RenderPropComponent>
{({counter}) => <p>Counter: {counter}</p>}
</RenderPropComponent>
);
};
If you want to read more extensively about render props pattern in React and their usage in React ecosystem check this post.
Test preparation
In order to test render prop component, we should first write one! Our component will fetch posts from an API and expose loading state and posts to a consumer component.
import React from 'react';
import PropTypes from 'prop-types';
import { fetchPosts } from './api';
export default class FetchPosts extends React.Component {
static propTypes = {
children: PropTypes.func.isRequired
};
state = { posts: [], loading: false };
async componentDidMount() {
this.setState({ loading: true });
const posts = await fetchPosts();
this.setState({ posts, loading: false });
}
render() {
return this.props.children({posts: this.state.posts, loading});
}
}
Writing the test
We are going to write our test using jest
and react-testing-library
but the same principles apply if you use something else to write your tests.
import React from 'react';
import { render } from 'react-testing-library';
import FetchPosts from './FetchPosts';
const mockPosts = [{ id: 1, title: 'Title' }];
jest.mock('./fetchPosts', () => Promise.resolve(mockPosts));
describe('FetchPosts component test', () => {
it('should expose loading and posts prop', () => {
const postsCallbackMock = jest.fn();
const { getByTestId } = render(
<FetchPosts>{postsCallbackMock}</FetchPosts>
);
expect(postsCallbackMock).toHaveBeenCalledWith({
loading: false,
posts: mockPosts
})
});
});
This is one a bit simpler way to test render prop component. Another way is to write a consumer component which renders something on the page and then expect it matches with the data that you received. For example:
import React from 'react';
import { render } from 'react-testing-library';
import FetchPosts from './FetchPosts';
const mockPosts = [{ id: 1, title: 'Title' }];
jest.mock('./fetchPosts', () => {
return new Promise(resolve => {
setTimeout(() => resolve(mockPosts), 100);
});
});
const FetchPostsConsumer = () => (
<FetchPosts>
{({loading, posts}) => {
if(loading) return <span data-testid="loading"></span>;
return posts.map(post => <p data-testid="post-title">{post.title}</p>)
}}
</FetchPosts>
);
describe('FetchPosts component test', done => {
it('should return correct loading and posts props', () => {
const postsCallbackMock = jest.fn();
const { getByTestId } = render(
<FetchPostsConsumer />
);
expect(getByTestId('loading').textContent).toBe('Loading');
setTimeout(() => {
expect(getByTestId('post-title').textContent).toBe('Title');
done()
})
});
});
At the beginning of this test, we are declaring what our fetchPosts
module is returning so we can have the same results on each test run (these tests are called deterministic). This mocked version of a function is resolving a promise but after some timeout, which gives us enough time to inspect the loading state later in our test.
Next, we are declaring a component that is using render prop component that we really want to test. After the component is rendered we are checking if loading text is present. After some time, we are checking if the correct post is being rendered as a result of render prop callback. This approach is a bit longer but in my opinion, it gives us a bit more user-oriented test which in the end is how users are going to use our component.
Conclusion
As you can see testing render prop component is not that difficult in the end. Since that kind of a component doesn't generate an output by itself we have to provide that missing part in our test and then do the assertions. A simpler way is to just provide a mock function and expect that it's called with correct parameters. Which approach do you like more? Share it in the comments below 👇
Top comments (1)
Thanks for the post. This is exactly what I needed!