Thinkster
React

Implementing a Tabs Component with React & Redux

While creating a basic tabs interface is quite simple, in this tutorial we'll be creating a tabs component that is also reusable & extensible

Valeri Karpov Up to date (Jan '17) React

This tutorial is a part of the "Build a Real World App with React & Redux" tutorial series.

Next up is filling out the functionality on the home page. When you're logged in, you don't see the global feed, you instead see a feed from users that you follow.

First off, let's implement these tabs on the home page. To get the user feed, you need to add a new method to agent.js:

Update src/agent.js
// ...
const Articles = {
  all: page =>
    requests.get(`/articles?limit=10`),
  byAuthor: (author, page) =>
    requests.get(`/articles?author=${encodeURIComponent(author)}&limit=5`),
  del: slug =>
    requests.del(`/articles/${slug}`),
  favoritedBy: (author, page) =>
    requests.get(`/articles?favorited=${encodeURIComponent(author)}&limit=5`),
  feed: () =>
    requests.get('/articles/feed?limit=10'),
  get: slug =>
    requests.get(`/articles/${slug}`)
};
// ...

Next, let's add 2 helper components to the 'MainView' component, one for each tab.

Update src/components/home/MainView.js
import ArticleList from '../ArticleList';
import React from 'react';
import agent from '../../agent';
import { connect } from 'react-redux';

const YourFeedTab = props => {
  if (props.token) {
    const clickHandler = ev => {
      ev.preventDefault();
      props.onTabClick('feed', agent.Articles.feed());
    }

    return (
      <li className="nav-item">
        <a  href=""
            className={ props.tab === 'feed' ? 'nav-link active' : 'nav-link' }
            onClick={clickHandler}>
          Your Feed
        </a>
      </li>
    );
  }
  return null;
};

const GlobalFeedTab = props => {
  const clickHandler = ev => {
    ev.preventDefault();
    props.onTabClick('all', agent.Articles.all());
  };
  return (
    <li className="nav-item">
      <a
        href=""
        className={ props.tab === 'all' ? 'nav-link active' : 'nav-link' }
        onClick={clickHandler}>
        Global Feed
      </a>
    </li>
  );
};

const mapStateToProps = state => ({
  ...state.articleList,
  token: state.common.token
});

const mapDispatchToProps = dispatch => ({
  onTabClick: (tab, payload) => dispatch({ type: 'CHANGE_TAB', tab, payload })
});

const MainView = props => {
  return (
    <div className="col-md-9">
      <div className="feed-toggle">
        <ul className="nav nav-pills outline-active">

          <YourFeedTab
            token={props.token}
            tab={props.tab}
            onTabClick={props.onTabClick} />

          <GlobalFeedTab tab={props.tab} onTabClick={props.onTabClick} />

        </ul>
      </div>

      <ArticleList
        articles={props.articles} />
    </div>
  );
};

export default connect(mapStateToProps, mapDispatchToProps)(MainView);

The YourFeedTab component will only show up if there's a token defined, that is, if the user is actually logged in. It'll also take in the current tab from the state, and use that to determine whether it should have the 'active' CSS class.

Both tabs are going to dispatch an 'CHANGE_TAB' action, which will change the current tab and attach a promise for the promise middleware to resolve.

In mapDispatchToProps(), we'll add a onTabClick function that will dispatch the 'CHANGE_TAB' action.

Now, let's add a handler for the 'CHANGE_TAB' action in the 'articleList' reducer.

Update src/reducers/articleList.js
export default (state = {}, action) => {
  switch (action.type) {
    // ...
    case 'CHANGE_TAB':
      return {
        ...state,
        articles: action.payload.articles,
        articlesCount: action.payload.articlesCount,
        tab: action.tab
      };
    case 'PROFILE_PAGE_LOADED':
    case 'PROFILE_FAVORITES_PAGE_LOADED':
    // ...
  }

  return state;
};

We also need to modify the Home component to initialize the current tab:

Update src/components/Home/index.js
// ...

const mapStateToProps = state => ({
  appName: state.common.appName,
  token: state.common.token
});

const mapDispatchToProps = dispatch => ({
  onLoad: (tab, payload) =>
    dispatch({ type: 'HOME_PAGE_LOADED', tab, payload }),
  onUnload: () =>
    dispatch({  type: 'HOME_PAGE_UNLOADED' })
});

class Home extends React.Component {
  componentWillMount() {
    const tab = this.props.token ? 'feed' : 'all';
    const articlesPromise = this.props.token ?
      agent.Articles.feed() :
      agent.Articles.all();

    this.props.onLoad(tab, articlesPromise);
  }

  // ...
}
// ...

So now you can go to the home page and change tabs. However, why does it show the global feed tab initially? The answer is that there's a race condition: props.token isn't set until the 'APP_LOAD' action gets handled, so we need to modify the 'App' component to not render the current view until it has managed to fetch the currently logged in user.

Update src/components/App.js
// ...

const mapStateToProps = state => ({
  appLoaded: state.common.appLoaded,
  appName: state.common.appName,
  currentUser: state.common.currentUser,
  redirectTo: state.common.redirectTo
});

// ...

class App extends React.Component {
  // ...
  render() {
    if (this.props.appLoaded) {
      return (
        <div>
          <Header
            appName={this.props.appName}
            currentUser={this.props.currentUser} />
          {this.props.children}
        </div>
      );
    }
    return (
      <div>
        <Header
          appName={this.props.appName}
          currentUser={this.props.currentUser} />
      </div>
    );
  }
}
// ...

Now the tabs work as expected, you can switch back and forth.

 

I finished! On to the next chapter