PROGRAMMING
TabView is one of the most popular UI elements found across a plethora of apps and websites. In this tutorial, I am going to explain how to develop a custom TabView component using Next.js and parallel routing supported by the framework.
React and Next.js both communities are moving towards what's called Server Components. Next.js v13.0 was truly transformational in the way things were done and first step towards this new approach. Parallel routing is what was introduced in this new architecture in the Next.js world. For this tutorial however, I will keep things simple, and use Client Components.
We will be using TypeScript as our choice of language. But first, let's learn a more about TabView and why to use it?
As explained on Material Design home page:
"TabViews are used to organized content across different screens and views."
There are some properties that need to be satisfied in order to make an efficient TabView element on UI:
But where do we use TabViews? In a lot of places to be honest. But here are a few popular examples:
By now it should be pretty clear that TabView is an important element in a UI. Like other elements in a UI design TabView helps us achieve a few things enumerated below:
You can read more about TabViews on the following links:
Enough talking about the design aspect of TabBar. Let's move to the development part of it. Shall we? Let's quickly set-up a working Next.js project.
Run the following code on your terminal:
1npm create-next-app@latest tab-bar-view
A side note before we begin: you can achieve the same functionality using children props feature of React. Although, the functionality achieved will be similar, doing it with parallel routing has some added advantages over using children props. Although, These advantages are very specific to Next.js, which I will discuss in the end.
Let us imagine that there is a analytics dashboard. On this dashboard page, there are four different sections, that show four different statistical data to the end user. To make life more complex, let us say that each of this section uses a different API from the backend, and different statistical algorithms to render the data.
There are two ways to render this dashboard page. First the simpler way, you make API call to all four APIs, perform calculations for all four data (maybe in parallel to load faster) and once all the data is available to us, we simply render the UI.
Now imagine if the load time and algorithmic complexity of one of the four APIs is very fast compared to the others, shouldn't we fetch this data as soon as it is ready and render at least the partial UI. Parallel routing helps us solve this exact problem. It helps you render different parts of a UI as and when they are available. The best part, each component in UI is then separated from the other, and you can lazy load the entire page with these components as and when their respective data is ready to be rendered.
In Next.js, parallel routing is achieved using a concept called Slots. Next.js routing architecture is heavily based on the file and folder names. Slots are just one another fancy term for one of the special conventions in file and folder naming.
You need to understand how to render pages and layout and in the newer version of Next.js.
To create slots simply create folders starting with '@' and then the name of the UI layout in your page. For our dashboard example, the four UI components and their respective folder names will be:
@trips
)@amounts
)@ratings
)@points
)Then we create a file called page.tsx
inside each of these folder. This file is a normal TSX
file that contains UI code. Our slots are ready to be used. But how do you use them? Simply pass them as children prop to the parent layout.tsx file in the folder. For our dashboard example, if the folder structure looked like this:
app
+-- layout.tsx
+-- @trips
| +-- page.tsx
+-- @amounts
| +-- page.tsx
+-- @ratings
| +-- page.tsx
+-- @points
| +-- page.tsx
We need to pass all our slots in the parent layout.tsx
file like this:
1'use client'; 2 3export default function Layout({ 4 trips, 5 team, 6 analytics, 7}: { 8 children: React.ReactNode 9 analytics: React.ReactNode 10 team: React.ReactNode 11}) { 12 return ( 13 <> 14 {children} 15 {trips} 16 {amounts} 17 {ratings} 18 {points} 19 </> 20 ) 21}
Notice how each slot becomes a children element inside the layout file.
We are going to make TabView that works like shown below:
Visiting document mentioned @ Next.js official docs to create a TabView we understand quickly that the functionality almost remains same if we want to implement a TabView.
We make sub-folders inside a parent folder. The parent folder is actually a slot and the sub-folders represent the tabs we want to create. This time the only difference is that inside the parent folder we create a layout.tsx
file that will be responsible to provide the shared TabView element to all the children. This layout.tsx
file will also take care of routing and rendering the content according to the TabBar item selected.
So, let us start implementing our TabView now.
Let's create a folder called @tabs
inside the app
folder.
Inside @tabs
folder let us create three folders called flights
, trips
, and explore
. Let us create a layout.tsx
file inside the @tabs
folder and page.tsx
inside each of the three sub-folders we created.
Our project structure should look similar to this:
app +-- layout.tsx +-- page.tsx +-- @tabs | +-- layout.tsx | | +-- flights | | | +-- page.tsx | | +-- trips | | | +-- page.tsx | | +-- explore | | | +-- page.tsx
Notice a layout.tsx
file inside the app folder. That is a must-have file which Next.js requires in order to render your UI. Removing this file, will result in a crashing of your app.
Let us create a folder called components
and then a sub-folder called tab-view
. Inside tab-view
folder, create three files called:
tab-view-props.ts
1export default interface TabViewProps { 2 tabs: { 3 id: number; 4 label: string; 5 }[]; 6 rootLink: string; 7}
The first field called tabs
contains the data for the tabs to render via the TabBar.
The second field called rootLinks
is used to automatically change the link and route the user to first tab whenever we navigate to any layout that has a TabView.
For our example, this will automatically route the user to the filghts
tab and link.
Next let us create the tab-view.tsx
file that contains the logic for rendering the TabBar and view:
1'use client';
2
3import {usePathname, useRouter } from 'next/navigation';
4import { useEffect, useState } from 'react';
5
6import TabViewProps from './tab-view-props';
7
8import { Subtitle } from '../typography';
9
10import styles from './styles.module.scss';
11
12export default function TabView(props: TabViewProps) {
13 const [activeTab, setActiveTab] = useState(props.tabs[0].label);
14 const router = useRouter();
15 const pathname = usePathname();
16 const pathnameArr = pathname.split('/');
17
18 useEffect(() => {
19 /**
20 * Whenever a {@link TabView} is first called, redirect the root layout to first child appended to the pathname.
21 */
22 if (pathnameArr.at(-1) === props.rootLink) {
23 router.push(`${pathname}/${props.tabs[0].label}`);
24 }
25 setActiveTab(pathnameArr.at(-1));
26 }, []);
27
28 /**
29 * This function handles the click event on any Tab of a {@link TabView}.
30 *
31 * @param tabData An object containing information about the tab clicked.
32 */
33 const handleTabClick = (tabData: { id: number; label: string }) => {
34 setActiveTab(tabData.label);
35 router.push(
36 pathnameArr
37 .slice(0, pathnameArr.length - 1)
38 .concat(tabData.label)
39 .join('/')
40 );
41 };
42
43 return (
44 <nav className={styles.tabBar}>
45 {props.tabs.map((tabData) => (
46 <button
47 key={tabData.id}
48 className={
49 activeTab === tabData.label
50 ? styles.tab__active
51 : styles.tab__inactive
52 }
53 type="button"
54 onClick={() => handleTabClick(tabData)}
55 >
56 <Subtitle
57 textColor={
58 activeTab === tabData.label ? '#141414' : '#505050'
59 }
60 width="100%"
61 >
62 {tabData.label}
63 </Subtitle>
64 </button>
65 ))}
66 </nav>
67 );
68}
Forget the Subtitle
component. That is just a custom Text component.
Let us understand the what this code does. First we simple map the tabsData
passed to us in the props and render a button with a label for each tab.
We set the className
on the each button and activeColor
property on Subtitle
based on whether the a particular tab item is active
on inactive
which will be later used in styling the components.
Then we have a function called handleTabClick
, which essentially does two things:
@tabs
folder. Proper routing means we render the correct UI for each tab item.Create a file called styles.module.scss
inside the tab-view
folder.
styles.module.scc
1.tabBar { 2 display: flex; 3} 4 5%tab { 6 height: 48px; 7 flex: 1; 8 background-color: #F7B6D4; 9 border: none; 10 cursor: pointer; 11} 12 13.tab__inactive { 14 @extend %tab; 15} 16 17.tab__active { 18 @extend %tab; 19 20 border-bottom: solid 2px #B0005C; 21} 22 23.tabContent { 24 padding: 32px 80px; 25}
A pretty simple SCSS
file. We make our tab bar a flex
display so that we can accommodate multiple items inline.
We give each tab item a flex
of 1
, to take equal space in the TabBar irrespective of the number of items.
Some padding, height and other basic CSS
attributes to style our TabBar look like the one shown earlier. One last thing to note is that whenever any tab item becomes active, we simple add a bottom-border to the button to give a feel of indicator to the user.
The final piece in the puzzle is to write the logic in our layout files.
First let's work on the main layout.tsx
file inside our app folder.
1'use client';
2
3import { ReactNode } from 'react';
4
5import './styles/global.scss';
6
7export default function MainLayout({ tabs }: { tabs: ReactNode }) {
8 return (
9 <html lang="en">
10 <body>
11 <div>{tabs}</div>
12 </body>
13 </html>
14 );
15}
Pretty straight forward. The root layout file in Next.js needs a mandatory <html>
or <body>
tag around the main content. Here inside the <body>
tag we are passing tabs
as children props. Don't get confused here. tabs
is just a variable name here. In Next.js documents you will find this variable named children
.
Now the layout.tsx
file inside @tabs
folder.
1'use client';
2
3import { type ReactNode } from 'react';
4
5import TabView from '../../components/tab-view/tab-view';
6
7export default function TabLayout({ children }: { children: ReactNode }) {
8 const tabsData = [
9 {
10 id: 0,
11 label: 'flights'
12 },
13 {
14 id: 1,
15 label: 'trips'
16 },
17 {
18 id: 2,
19 label: 'explore'
20 }
21 ];
22
23 return (
24 <div>
25 <TabView tabs={tabsData} rootLink="accounts" />
26 {children}
27 </div>
28 );
29}
We first create an array containing objects respective to each tab item that we want to render. Because we are iterating over this array in our TabView implementation, we need keys to uniquely identify each tab item in the array. The id
field is used for the same exact reason.
Then we render our TabView
component passing in the tabsData
array. Also, remember, handleTabClick
function earlier and its second functionality to automatically route the user to correct URL, whenever any Tab View is opened first. It does with the help of a root link passed in the rootLink
prop.
A quick hack: Whenever you are working with arrays in React, always give them a default id
attribute, be it a number index or string based.
Remember I told you that you can achieve the same functionality using children props instead of parallel routing. If you notice, you will understand that even parallel routing is similar to children props as it also renders all the tab items content by passing them as children props to the parent layout component.
Then why choose one over another? Let's see:
Children props are great way to pass an entire component to a parent component without worrying about the implementation of children component. It is actually an amazing concept and used in HOC too, which is an advanced React composition pattern.
The only advantage I see in using parallel routing is that you conform to the notion of Next.js. Even if we use children props, we need to create separate files for each tab item in their respective TSX files. So in terms of code, I don't think there is much difference.
But creating separate folders inside a slot folder, and letting Next.js take care of the children prop rendering helps us achieve two things in specific:
In the end, it's a personal choice but I prefer using Next.js parallel routing as it can also help us achieve dashboard kinda UI in future if needs arise using all the awesomeness that Next.js provides out of the box. Achieving similar functionality using children props would be a bit harder in comparison.
That's all. We have successfully implemented a TabBar using parallel routing in Next.js.
You can also check the final working code on GitHub @ Why-So-Serious-Life/create-tab-bar-parallel-routes-nextjs
If you want to learn how to create your own TextField using React.js, you can checkout Create a custom TextField Component with Floating Labels.
Finally, I want to leave you with a thought.
What is life if not a drink in different bars?
Deciphering the Code: Where Brackets Are Puzzles, and Debugging Is Detective Work! Welcome to the world of programming, where lines of code create digital magic. Whether you're a seasoned coder or a newbie, get ready to explore the syntax symphony, tackle bugs like a pro, and embark on quests to build the next big thing in tech. 💻🚀