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.
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
- 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 ablogPostObject
Let's create a file atsrc/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 ourpages/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 forFavorite Posts
. By the end of the series, we want users tofavorite
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 codeid
s and use them as favourite posts. Let's create aFavoritePosts
component in thepages/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 atsrc/components/Tabs.js
. We will first build this component inpages/index.js
and when it is done we will move it tosrc/components
. For now leavesrc/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
andtabContents
. SotabNames
are the tabs that will be selectable and when the firsttabName
is selected, we show the firsttabContent
. If the secondtabName
is selected we show the secondtabContent
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 andtabContents[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 showingtabNames
.
//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 theonClick
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>
);
})}
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.
- 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
- what
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 the
tabNames
and show the selectedtabName
'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!