< Previous: Time for a Task: Reading from Three Feeds   Next: Refactoring our Ajax Code: Don't Repeat Yourself >

How to Upgrade Our App to Read Three Feeds

OK, I hope you took the time to attempt the task yourself, because it will really help identify which parts you knew well and which not so well. Anyway, I promised I'd walk you through the solution, so here goes!

The first thing to do is upgrade the initial state of our component so that we have three arrays for each of the view modes: one for commits, one for forks, and one for pulls. It also needs a value to distinguish which view mode is currently active. So, I modified my constructor to this:

src/pages/Detail.js

this.state = {
    mode: 'commits',
    commits: [],
    forks: [],
    pulls: []
};

The second thing to do is upgrade the componentWillMount() so that it makes three GitHub calls. For now – the MVP, as it were – we'll just duplicate the code three times. Don't worry, we'll clean this up soon.

Here's the new componentWillMount() method:

src/pages/Detail.js

componentWillMount() {
    ajax.get('https://api.github.com/repos/facebook/react/commits')
        .end((error, response) => {
            if (!error && response) {
                this.setState({ commits: response.body });
            } else {
                console.log('Error fetching commits', error);
            }
        }
    );

    ajax.get('https://api.github.com/repos/facebook/react/forks')
        .end((error, response) => {
            if (!error && response) {
                this.setState({ forks: response.body });
            } else {
                console.log('Error fetching forks', error);
            }
        }
    );

    ajax.get('https://api.github.com/repos/facebook/react/pulls')
        .end((error, response) => {
            if (!error && response) {
                this.setState({ pulls: response.body });
            } else {
                console.log('Error fetching pulls', error);
            }
        }
    );
}

Next up, we need three rendering methods so that each view type shows relevant information. I've called these renderCommits(), renderForks() and renderPulls():

src/pages/Detail.js

renderCommits() {
    return this.state.commits.map((commit, index) => {
        const author = commit.author ? commit.author.login : 'Anonymous';

        return (<p key={index}>
            <strong>{author}</strong>:
            <a href={commit.html_url}>{commit.commit.message}</a>.
        </p>);
    });
}

renderForks() {
    return this.state.forks.map((fork, index) => {
        const owner = fork.owner ? fork.owner.login : 'Anonymous';

        return (<p key={index}>
            <strong>{owner}</strong>: forked to
            <a href={fork.html_url}>{fork.html_url}</a> at {fork.created_at}.
        </p>);
    });
}

renderPulls() {
    return this.state.pulls.map((pull, index) => {
        const user = pull.user ? pull.user.login : 'Anonymous';

        return (<p key={index}>
            <strong>{user}</strong>:
            <a href={pull.html_url}>{pull.body}</a>.
        </p>);
    });
}

Note: you will probably need to adjust the renderForks() method so that the link sits on the same line as the "forked to" otherwise React will not put any space between the words.

That isolates the rendering for each view type in its own method, which means we now just need to make render() choose which one to show. I'm using the mode key in the component state to decide which to show, and I'll let it have three values: "commits", "forks" and "pulls".

With that in mind, here's how render() should look:

src/pages/Detail.js

render() {
    let content;

    if (this.state.mode === 'commits') {
        content = this.renderCommits();
    } else if (this.state.mode === 'forks') {
        content = this.renderForks();
    } else {
        content = this.renderPulls();
    }

    return (<div>
        <button onClick={this.showCommits.bind(this)}>Show Commits</button>
        <button onClick={this.showForks.bind(this)}>Show Forks</button>
        <button onClick={this.showPulls.bind(this)}>Show Pulls</button>
        {content}
    </div>);
}

You can see three buttons at the end of that method that call three as-yet-undefined methods when they are clicked: showCommits(), showForks() and showPulls(). All these need to do is change the mode state key to have the component refresh with different data:

src/pages/Detail.js

showCommits() {
    this.setState({ mode: 'commits' });
}

showForks() {
    this.setState({ mode: 'forks' });
}

showPulls() {
    this.setState({ mode: 'pulls' });
}

Remember, changing the state or props of a component causes it to re-render, which means clicking those buttons will update our output as expected.

Before I move on, here's a complete copy of Detail.js at this point in the project. If you're having problems, you should be able to compare my version against yours and see what's missing:

src/pages/Detail.js

import React from 'react';
import ajax from 'superagent';

class Detail extends React.Component {
    constructor(props) {
        super(props);

        this.state = {
            mode: 'commits',
            commits: [],
            forks: [],
            pulls: []
        };
    }

    componentWillMount() {
        ajax.get('https://api.github.com/repos/facebook/react/commits')
            .end((error, response) => {
                if (!error && response) {
                    this.setState({ commits: response.body });
                } else {
                    console.log('Error fetching commits', error);
                }
            }
        );

        ajax.get('https://api.github.com/repos/facebook/react/forks')
            .end((error, response) => {
                if (!error && response) {
                    this.setState({ forks: response.body });
                } else {
                    console.log('Error fetching forks', error);
                }
            }
        );

        ajax.get('https://api.github.com/repos/facebook/react/pulls')
            .end((error, response) => {
                if (!error && response) {
                    this.setState({ pulls: response.body });
                } else {
                    console.log('Error fetching pulls', error);
                }
            }
        );
    }

    showCommits() {
        this.setState({ mode: 'commits' });
    }

    showForks() {
        this.setState({ mode: 'forks' });
    }

    showPulls() {
        this.setState({ mode: 'pulls' });
    }

    renderCommits() {
        return this.state.commits.map((commit, index) => {
            const author = commit.author ? commit.author.login : 'Anonymous';

            return (<p key={index}>
                <strong>{author}</strong>:
                <a href={commit.html_url}>{commit.commit.message}</a>.
            </p>);
        });
    }

    renderForks() {
        return this.state.forks.map((fork, index) => {
            const owner = fork.owner ? fork.owner.login : 'Anonymous';

            return (<p key={index}>
                <strong>{owner}</strong>: forked to
                <a href={fork.html_url}>{fork.html_url}</a> at {fork.created_at}.
            </p>);
        });
    }

    renderPulls() {
        return this.state.pulls.map((pull, index) => {
            const user = pull.user ? pull.user.login : 'Anonymous';

            return (<p key={index}>
                <strong>{user}</strong>:
                <a href={pull.html_url}>{pull.body}</a>.
            </p>);
        });
    }

    render() {
        let content;

        if (this.state.mode === 'commits') {
            content = this.renderCommits();
        } else if (this.state.mode === 'forks') {
            content = this.renderForks();
        } else {
            content = this.renderPulls();
        }

        return (<div>
            <button onClick={this.showCommits.bind(this)}>Show Commits</button>
            <button onClick={this.showForks.bind(this)}>Show Forks</button>
            <button onClick={this.showPulls.bind(this)}>Show Pulls</button>
            {content}
        </div>);
    }
}

export default Detail;

As you can see, there are no great surprises in there – it's just taking what we already have and repeating it three times over.

Buy the book for $10

Get the complete, unabridged Hacking with React e-book and take your learning to the next level - includes a 45-day no questions asked money back guarantee!

If this was helpful, please take a moment to tell others about Hacking with React by tweeting about it!

< Previous: Time for a Task: Reading from Three Feeds   Next: Refactoring our Ajax Code: Don't Repeat Yourself >

Copyright ©2016 Paul Hudson. Follow me: @twostraws.