#12 Building a Tabs component - Delightful React

#12 Building a Tabs component - Delightful React

Now that we know enough about useState and state variables, let us pick up the pace now and build a Tabs component that shows different content based on the selected tab.

Group-1.png

By the time we are done with this chapter, we will have built a nice Tabs component like this. And we will be able to toggle between "All Posts" and "Featured Posts".


*Note: This article is a part of the Delightful React Series, as part of which, I am releasing one chapter every day for 25 days. Please support me in anyway you can! *


Building a Tabs component

Asset_35.png

  • Given a list of tab names and their tab contents, a tabs component should be able to display a tab content when it’s corresponding tab name is clicked.

It's a very common component and building components like these strengthens our React foundations.

Let's refactor our app first

Our home page is showing a list of all posts right now and it looks like this.

// pages/index.js
// ... 

function Home(props) {
  return (
    <div>
      <Heading>All Posts</Heading>
      <div className="post-list">
        {BLOGPOSTS.map((blogPostObject) => {
          return (
            <div key={blogPostObject.title} className="post-list-item">
              <img src={blogPostObject.img} />
              <div>
                <h1>{blogPostObject.title}</h1>
              </div>
            </div>
          );
        })}
      </div>
    </div>
  );
}

export default Home;

**You can use this code sandbox embed as a starting point for this chapter. Fork it and follow the next instructions.



  • We eventually want to show two tabs of posts.
  • And both tabs show the posts in the same way, except, favourite posts will contain only the posts that a user has liked. So let's first create a reusable ListItem component that can show the fields of a blogPostObject Let's create a file at src/components/ListItem.js and put in the following content.
//src/components/ListItem.js
export default function ListItem(props) {
  const { blogPostObject } = props;
  // const blogPostObject = props.blogPostObject
  const { img, title, content = "", tags } = blogPostObject;
  return (
    <div className="post-list-item">
      <img src={img} />
      <div>
        <h1>{title}</h1>
        <p>{content.slice(0, 100)}</p>
        <div className="tags">
          {tags.map((tag) => (
            <span key={tag}>{tag}</span>
          ))}
        </div>
      </div>
    </div>
  );
}

Note: This isn't any thing new, we used the same UI in our previous chapters. We just moved all of it into it's own component so that we can reuse it.

  • Now let's create a component called AllPosts inside our pages/index.js file to show all the posts.
//pages/index.js

import ListItem from '../src/components/ListItem'

  function AllPosts() {
    return (
      <div>
        <Heading>All Posts</Heading>
        <div className="post-list">
          {BLOGPOSTS.map((blogPostObject) => {
            return (
              <ListItem
                key={blogPostObject.title}
                blogPostObject={blogPostObject}
              />
            );
          })}
        </div>
      </div>
    );
  }
  • We want one tab for All Posts and another tab for Favorite Posts. By the end of the series, we want users to favorite some posts for them to read often and find them all in this section. For now we will start off with a list of hard code ids and use them as favourite posts. Let's create a FavoritePosts component in the pages/index.js file.
const FAVORITE_BLOGPOSTS = BLOGPOSTS.filter((blogPostObject) =>
  [1, 3, 4, 5].includes(blogPostObject.id)
);
function FavoritePosts() {
  return (
    <div>
      <Heading>Favorite Posts</Heading>
      <div className="post-list">
        {FAVORITE_BLOGPOSTS.map((blogPostObject) => {
           return (
            <ListItem
              key={blogPostObject.title}
              blogPostObject={blogPostObject}
            />
          );
        })}
      </div>
    </div>
  );
}
  • We can now render them in our Home component like so
  function Home(props) {
    return (
        <AllPosts />
    );
  }

Of course, we only want to show either AllPosts or FavoritePosts at any given time. So for that, let's build our Tabs component.

Let’s get started

  • We have been building components in our src/components folder. So let's continue that tradition and create a new file for our Tabs component at src/components/Tabs.js. We will first build this component in pages/index.js and when it is done we will move it to src/components. For now leave src/components/Tabs.js empty.

  • Our Tabs component needs to have a state variable to track which tab is currently selected. So let's create a state variable for that purpose.

//pages/index.js

// create the component here temporarily. 
// we will move this to src/components shortly
function Tabs(){
  const [currentTabIndex, setCurrentTabIndex ] = useState(0);
  return <div>
    <div className="tabs">      
    </div>
  </div>
}
  • Let’s create a couple arrays of equal size, tabNames and tabContents. So tabNames are the tabs that will be selectable and when the first tabName is selected, we show the first tabContent. If the second tabName is selected we show the second tabContent and so on.
//pages/index.js

const tabNames = ["All Posts", "Favorite Posts"]
const tabContents = [<AllPosts/>, <FavoritePosts/>];

function Tabs(props){
    ...
}
  • Let’s use the currentTabIndex state variable and tabContents[currentTabIndex] to show the selected tab.
//pages/index.js

function Tabs(props){
  const [currentTabIndex, setCurrentTabIndex ] = useState(0);
  return <div>
      <div className="tabs">       
      </div>
      {tabContents[currentTabIndex]}
  </div>
}
  • We can now render this on our Home page. Of course it’s doesn’t do much yet since we aren’t showing tabNames.
//pages/index.js
function Home(props) {
    return (
        <Tabs />
    );
}
  • Let’s fix that by showing the elements of tabNames using the .map method.
//pages/index.js
// ...

function Tabs(props){
  const [currentTabIndex, setCurrentTabIndex ] = useState(0);
  return <div>
      <div className="tabs">
        {tabNames.map((tabName) => {
          return <span 
                className="tab"
                key={tabName}                
              >
              {tabName}
            </span>
        })}      
      </div>
      {tabContents[currentTabIndex]}
  </div>
}
  • Finally let’s make our tabNames elements clickable and make them modify the state variable using the onClick event.
//pages/index.js

function Tabs(props){
  const [currentTabIndex, setCurrentTabIndex ] = useState(0);
  return <div>
      <div className="tabs">
        {tabNames.map((tabName, index) => {
          return <span 
                className="tab"
                key={tabName}
                onClick={function(){
                  setCurrentTabIndex(index)
                }}
              >
              {tabName}
            </span>
        })}      
      </div>
      {tabContents[currentTabIndex]}
    </div>
}

Awesome, now our Tabs component is able to toggle across the two tabs and show the corresponding tab content.

Wait a minute though

Is the Tabs component reusable? Well, we are using tabNames and tabContents as hardcoded values which means that this component will always show the same content no matter where it is used. Instead our Tabs component should be able to work with any tabNames andtabContents.

  • Let’s pass tabNames andtabContents as props.
//pages/index.js
function Home(props) {
    return (
        <Tabs  
             tabNames={["All Posts", "FavoritePosts"]}
             tabContents={[<AllPosts/>, <FavoritePosts/>]}
             />
    );
}
  • Finally, let’s use tabNames andtabContents from props.
// pages/index.js

function Tabs(props) {
    const { tabNames, tabContents } = props;
    //...
}

Almost there!

Our Tabs component is completely reusable now and doesn't depend on AllPosts or FavoritePosts components anymore. It can now be moved to src/components/Tabs. Let's do that now.

//src/components/Tabs.js

import { useState } from "react";

function Tabs(props) {
  const { tabNames, tabContents } = props;
  const [currentTabIndex, setCurrentTabIndex] = useState(0);
  return (
    <div>
      <div className="tabs">
        {tabNames.map((tabName, index) => {
          const isSelected = index === currentTabIndex;
          return (
            <span
              className={isSelected ? "tab active" : "tab"}
              key={tabName}
              onClick={function () {
                setCurrentTabIndex(index);
              }}
            >
              {tabName}
            </span>
          );
        })}
      </div>
      {tabContents[currentTabIndex]}
    </div>
  );
}

export default Tabs;

Active tab

It would be nice if we are able to highlight the selected tab. I already added some specific styling if an element has tab class and active class. So let’s add in some logic to add the active class if the currentTabIndex equals the index in the map function.

//src/components/Tabs.js


{tabNames.map((tabName, index) => {
    const isSelected = index === currentTabIndex
    return (
      <span
        className={isSelected ? "tab active" : "tab"}
        key={tabName}
        onClick={function () {
          setCurrentTabIndex(index);
        }}
      >
        {tabName}
      </span>
    );
})}

Screenshot 2021-01-24 at 9.46.14 AM.png

Awesome, we now have a fully functional tab component!

Plan your components well

We went for a quick and dirty approach to build this component, because this is one of the first times we built a big component in this series.

  • However, you can plan out components much better if you can visualise what your component is trying to do.
  • Components are just like functions remember? When we plan functions in our mind, we think of input, output and body of the function.

Asset_15.png

  • Similarly, when building components we always need to consider
    • what props our component should work with ,
    • what state it should have and
    • what our component returns

Asset_14.png

Once we understand that, we can plan the rest of the body around that.

  • The Tabs component needs to receive which tabs to show as props.
  • The Tabs component needs to track the current tab that is being show. So let’s create a state variable for that.
  • The Tabs component will need to show all thetabNames and show the selected tabName's content.

No matter which component we build in the future, we can ask ourselves these questions and plan our components well.

Great work so far!

Thanks for following me so far! We are beginning to some incredible components and hooks now. In the next chapter we will talk about Forms in React. Stay tuned!

Thanks and Please support my work

  • Writing blog posts and making videos is an effort that takes many hours in my day. I do it because I love teaching and make great content.
  • However, I need your support too. Please support my work and follow my social accounts to help me continue to make great content.
  • Here are my social links.

Follow me on Twiter

Subscribe to my channel on Youtube

Thank you!