loading...

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

patrickgordon profile image Patrick ・3 min read

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.

Discussion

pic
Editor guide
Collapse
chetankumars profile image
chetankumars

I want to store a blob type API response in a cache for a few minutes.

Collapse
chadsteele profile image
Chad Steele

Great article! Thanks!
I think you'll like this alt to redux as well.
dev.to/chadsteele/eventmanager-an-...