For many developers, implementing a sticky header on a table before the CSS property position: sticky
was challenging. In this article, we are going to look at building a sticky header using React Hooks.
When creating a sticky header for tables, most frontend developers use the <table />
component and the CSS position
property, however, in this post we are going to build a table with a sticky header using native React Hooks.
This tutorial assumes that the reader has:
Begin by installing the official React application scaffold tool, create-react-app
:
npm install -g create-react-app
You can verify the installation by running create-react-app
this should ask you to specify the directory name, in our case, we will be building a table of countries with a sticky header that’s built by React Hooks. We will be building the application as a <Table />
component that takes in the data for the header table and the data for the application, in our case, the <Table />
will take in a list of countries and some information about the country when hovered on by the user.
React allows the use of hooks with functional components, and as a result, we will be creating the following files:
Table.js
— This component will contain the table and sticky hook for our applicationData.js
— This component will contain the data to be passed as props in our applicationstickyHeader.js
— This will be a custom hook for making our header stickyWe will be using create-react-app
to bootstrap our project. To create a new project using the create-react-app
boilerplate, run the command in your preferred terminal:
create-react-app sticky-header-app
The name “sticky-header-app” is used as our project name for this tutorial, it can be replaced with whatever name you choose to use.
Next, navigate into your project directory and start your development server by running:
cd sticky-heade-app && npm start
The command above opens up a browser tab that will display the default boilerplate application.
In the src
folder, let’s create a new file Data.js
. Note that the file can be whatever you want and how you see fit.
Here, we export a constant HeaderTable
, an array with country, code, and area as objects, next we export another array of objects that contain a few countries, code, and their total land area:
export const HeaderTable = ["Country", "Code", "Area"]; export const tableData = [ { country: "Albania", code: "ALB", area: "28748 km2" }, { country: "Algeria", code: "AG", area: "2381740 km2" }, { country: "Brazil", code: "BR", area: "8,515,767 km2", }, { country: "Bulgaria", code: "BG", area: "110,993.6 km2", }, { country: "China", code: "CN", area: "9,596,961 km2", }, { country: "Denmark", code: "DK", area: "42,933 km2", }, { country: "USA", code: "US", area: "9,833,520 km2", } ]; export const countryData = [ { country: "China", data: `China, is a country in East Asia. It is the world's most populous country, with a population of around 1.4 billion in 2019.` }, { country: "Albania", data: `The land was first settled by a tribe of people called the Illyrians in about 2000 BC. The Romans were one of the first empires to conquer the land and Albania was part of first the Roman Empire and then later the Byzantine Empire or Eastern Roman Empire.` }, { country: "Algeria", data: `In ancient times Algeria was known as Numidia. The Numidians were known for their army which rode horses, or cavalry. During the Middle Ages, Algeria was lead by various tribes and Berber dynasties` }, { country: "Brazil", data: `Brazil was officially "discovered" in 1500, when a fleet commanded by Portuguese diplomat Pedro Álvares Cabral, on its way to India, landed in Porto Seguro, between Salvador and Rio de Janeiro.` }, { country: "Bulgaria", data: `Bulgarian history really starts with the Thracians. The Thracians were a group of Indo-European tribes that lived throughout the Balkan Peninsula from about 1000 BC.` }, { country: "Denmark", data: `Denmark has been inhabited by the Danes since its early prehistoric history. The Vikings rose to power during the 9th century. Over the next 300 years Vikings would raid and explore areas around Denmark all the way to the island of England.` }, { country: "USA", data: `The United States of America (USA) is a sovereign country in North America. It is commonly called the United States (U.S.) or America. There are forty-eight states that border each other and Washington, D.C., the capital district. These states are between the Pacific and Atlantic Oceans.` }, ]
In the code above, we initialized an array called HeaderTable
in which the data for our application which includes country, their code, and area. We also exported a data object which includes some information on the country. Next, we will build our custom StickyHeader
hook.
StickyHeader
hookIn this component, we will build a custom hook sticky header for our header table, we will add event listeners to handle scroll and enforce stickiness:
import { useState, useEffect, useRef, useCallback } from "react"; const StickyHeader = (defaultSticky = false) => { const [isSticky, setIsSticky] = useState(defaultSticky); const tableRef = useRef(null); const toggleSticky = useCallback( ({ top, bottom }) => { if (top <= 0 && bottom > 2 * 68) { !isSticky && setIsSticky(true); } else { isSticky && setIsSticky(false); } }, [isSticky] ); useEffect(() => { const handleScroll = () => { toggleSticky(tableRef.current.getBoundingClientRect()); }; window.addEventListener("scroll", handleScroll); return () => { window.removeEventListener("scroll", handleScroll); }; }, [toggleSticky]); return { tableRef, isSticky }; }; export default StickyHeader;
In the code block above, we imported the native React useState
update the initial state of the application, which we’ve defined as isSticky
, next we initialized the object tableRef
as a reference to the table element.
We initialized a constant handleScroll
to detail the position of the table on a screen and check for the current size of the header. The number 65 is the height of the header, the if…else
statement simply checks for the value of isSticky
and returns a new value if it changes.
Finally, we added an event listener to handle the user’s scroll events, next we’ll build the Table
component to handle the user data as props and render our application.
In this component, we will be building a functional component, that takes in parameters from our StickyHeader
component and renders our application:
import React, { useState } from "react"; import StickyHeader from "./StickyHeader.js"; import { countryData } from "./Data"; export default function Table({ headers = [], data = [] }) { const { tableRef, isSticky } = StickyHeader(); const [display, setDisplay] = useState(false); const [countryDetails, setData] = useState({ countryHistroy: null, }); const openDetails= (e) => { countryData.forEach(details => { if (details.country === e.target.textContent) { setData({ countryHistroy: details.data, }) } }); setDisplay(true); }; const closeDetails= () => { setDisplay(false); }; const renderHeader = () => ( <thead> <tr> {headers.map((item) => ( <th key={item}>{item}</th> ))} </tr> </thead> ); return ( <div> {isSticky && ( <table className="sticky" style={{ position: "fixed", top: 0, left: 0, right: 0 }} > {renderHeader()} </table> )} <table ref={tableRef}> {renderHeader()} <tbody> {data.map((item) => ( <tr key={item.code}> <td className="country" onMouseEnter={openDetails} onMouseOut={closeDetails}>{item.country}</td> <td>{item.code}</td> <td>{item.area}</td> </tr> ))} </tbody> </table> <div className="country-details" style={{display: display ? "block" : "none"}}> <p className="country-description"> {countryDetails.countryHistroy} </p> </div> </div> ); }
In this component, we initialized the StickyHeader
component using two parameters, tableRef
and isSticky
, the tableRef
references the table element where we’ll have a sticky header while the isSticky
checks the scroll events in the page.
The <table />
component takes the .sticky
style to differentiate the sticky header from the original table, to conclude our application we’ll need to update our App.js
file to render our individual components using the Table
component like this:
import React from "react"; import Table from "./Table"; import { HeaderTable, tableData } from "./Data"; import "./styles.css"; export default function App() { return ( <div className="App"> <Table headers={HeaderTable} data={tableData} /> </div> ); }
In the code block above, we are first importing our HeaderTable
and tableData
, next, we rendered the Table
component, passing the headerTable
and tableData
as props.
At the end of this, our app header should remain sticky while we scroll through our countries table, it should look like the video below:
In this post, we’ve learned how to use hooks to build a sticky header, we’ve also seen how to pass data from components and render it as props. You can read more React articles here on the LogRocket blog. The code used in this article can be found on CodeSandbox.
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowBuild scalable admin dashboards with Filament and Laravel using Form Builder, Notifications, and Actions for clean, interactive panels.
Break down the parts of a URL and explore APIs for working with them in JavaScript, parsing them, building query strings, checking their validity, etc.
In this guide, explore lazy loading and error loading as two techniques for fetching data in React apps.
Deno is a popular JavaScript runtime, and it recently launched version 2.0 with several new features, bug fixes, and improvements […]
One Reply to "Using React Hooks to create sticky headers"
I recommend passing the ref to the hook instead of from the hook to the implementation. It’s a little more resilient bc you know exactly what the ref is.