All Articles

React, Redux, and API's Part Two: React Only (DRY)

It’s been a long time between posts, mostly due to buying a house and working feverishly on my side project Albert. However, there’s no time like the present to get back in to it…

In the first post of the series, I showed how you can interface with an API in React. One of the main problems with this approach is that if you have multiple containers that need to talk to an API then you will be duplicating a lot of the same code.

In this post, we will take a look at how you can still talk to APIs with React, but in a DRY manner.

Abstract Common Code

Let’s look back at the example from the first post:

// Posts.js
import React, { Component } from "react";

import PostList from "./PostList";

class Posts extends Component {
	state = {
		posts: []
	}

	async componentDidMount() {
		const fetchConfig = {
			method: "GET",
			headers: new Headers({ "Content-Type": "application/json" }),
			mode: "cors"
		}

		const response = await fetch("https://jsonplaceholder.typicode.com/posts/", fetchConfig);
		
		if (response.ok) {
			const posts = await response.json();
			this.setState({ posts });
		} else {
			console.log("error!", error);
		}
	}

	render() {
		const { posts } = this.state;

		return (
			<PostList posts={posts} />
		)
	}
}

Now, imagine we also want to fetch comments from the same API. We would have to copy all the code for handling the config, and responses to a Comments container. You could play that scenario out for however many other different endpoints you need to call for.

An alternative is to abstract the common code. For example let’s create a new file apiHelper.js:

// apiHelper.js
export const SUCCESSFUL_STATUS = "success";
export const FAILED_STATUS = "failed";

const apiHelper = async ({ method, endpoint }) => {
	const fetchConfig = {
		method,
		headers: new Headers({ "Content-Type": "application/json" }),
		mode: "cors"
	}

	const response = await fetch(`https://jsonplaceholder.typicode.com/${endpoint}/`, fetchConfig);
	
	if (response.ok) {

		try {
			const data = await response.json();
			
			return {
				status: SUCCESSFUL_STATUS,
				data
			}
		} catch (error) {
			return {
				status: FAILED_STATUS,
				error
			}
		}

	} else {
		return {
			status: FAILED_STATUS
		}
	}
}

export default apiHelper;

Here we’ve moved all the handling from PostList to the helper and made it take some parameters.

Now see how Posts and Comments would look:

// Posts.js
import React, { Component } from "react";

import apiHelper, { SUCCESSFUL_STATUS } from "../utils/apiHelper";
import PostList from "./PostList";

class Posts extends Component {
	state = {
		posts: []
	}

	componentDidMount() {
		const { status, data } = apiHelper({ method: "GET", endpoint: "posts" });
		
		if (status === SUCCESSFUL_STATUS) {
			this.setState(() => ({ posts: data }));
		}
	}

	render() {
		const { posts } = this.state;

		return (
			<PostList posts={posts} />
		)
	}
}
// Comments.js
import React, { Component } from "react";

import apiHelper, { SUCCESSFUL_STATUS } from "../utils/apiHelper";
import CommentList from "./CommentList";

class Comments extends Component {
	state = {
		comments: []
	}

	componentDidMount() {
		const { status, data } = apiHelper({ method: "GET", endpoint: "comments" });
		
		if (status === SUCCESSFUL_STATUS) {
			this.setState(() => ({ comments: data }));
		}
	}

	render() {
		const { comments } = this.state;

		return (
			<CommentList comments={comments} />
		)
	}
}

As you can see there is minimal work required to make this much more flexible without repeating ourselves.

Bonus

What if you wanted to interface with multiple APIs but keep the duplication minimal? Here’s an example of how you might refactor apiHelper.js to do just that:

// apiHelper.js
export const SUCCESSFUL_STATUS = "success";
export const FAILED_STATUS = "failed";

const buildAPIHelper = (args) =>  async ({ method, endpoint }) => {
	const {
		baseURL,
		headers = new Headers({ "Content-Type": "application/json" }) // some sane defaults
	} = args;

	const fetchConfig = {
		method,
		headers,
		mode: "cors"
	}

	const response = await fetch(`${baseURL}${endpoint}`, fetchConfig);
	
	if (response.ok) {

		try {
			const data = await response.json();
			
			return {
				status: SUCCESSFUL_STATUS,
				data
			}
		} catch (error) {
			return {
				status: FAILED_STATUS,
				error
			}
		}

	} else {
		return {
			status: FAILED_STATUS
		}
	}
}

export const firstAPIHelper = buildAPIHelper({ 
	baseURL: "https://jsonplaceholder.typicode.com/",
});

export const secondAPIHelper = buildAPIHelper({
	baseURL: "https://api.patrick-gordon.com/" 
	headers: new Headers({ "Content-Type": "application/json", "Authorization": "bearer someKey" })
});

Up Next

In the next part of the series we will introduce Redux into the mix and look at how we can talk to an API using Redux.

Until then, cheers,

— Patrick.

Published 27 Oct 2018

Front-end software developer; husband; bicycle rider.
Patrick Gordon on Twitter