React components are the building blocks for creating UI in React.
There are different patterns that emerged over the years.
Today, we’re going to take a look at one of the most exciting UI component-building patterns: headless components.
Headless components aren’t necessarily specific to React — they’re just patterns that help to build reusable UI components.
Before building our example, we’ll first define what headless components are:
A component that doesn’t have a UI, but has the functionality.
What does that mean exactly?
Basically, headless components include anything you’d use to build a table component with these functionalities:
There are two options for building this kind of component.
The smart component will get the table data as input, store it in its internal state, and then do all the magical functionalities on the data.
It’ll also create a UI for the table and show the table in the DOM when users search for data and internal state updates, or fetch remote data and update the table.
If we want another table component in another page with the same table features but a different UI, we’ll need to reuse the logic for a totally different UI.
There are several ways to do this:
How? I’ll explain.
As I mentioned before, a headless component doesn’t care about the UI. Instead, headless components care about functionality. You can easily reuse the smartness associated with these components and isolate the UI component for reusability separately.
Let’s take a look at our example for creating a table.
The headless component just exposes methods to sort, filter, and perform all functionality on the data. It also transforms the data into an easy format to just run through as table rows.
Then, a separate UI component — a dump component — renders the table. Whenever there are some data changes, this dump component re-renders.
In this way, we can reuse both logic and UI.
You need headless components when you’re building a component library. Dictating UI on the end user is always bad — let the end user make the UI, and handle the functionality yourself.
Headless components are also useful when you’re building the same functionality with different UI in your application. For example, headless components are good for dropdown components, table components, and tabs components.
If you don’t have multiple UI for the same functionality in your application, or if you are not building a reusable component library for others to use, then headless components may not be necessary.
Essentially, headless UI uncouples the UI and the functionality and makes each of the pieces reusable separately.
Now, let’s build a react-countdown
headless component and see how it works.
React has three advanced patterns to build highly-reusable functional components.
This includes higher order components, render props components, and custom React Hooks.
We’ll see both render props components and React Hooks in our example.
Before building the headless component, let’s first build a simple React countdown and then reuse the functionality from it to create our reusable headless component.
Specs for our React-dropdown component:
Pretty simple, right?
Let’s dive into the code.
// App.js import React from "react"; // Export the SimpleCOuntdown component, we have to build this component :) import SimpleCountdown from "./components/simple-countdown"; function App() { // Create a future date and pass in to the SimpleCountdown const date = new Date("2021-01-01"); // New year - Another 3xx days more :) return ( <div className="App"> <SimpleCountdown date={date} /> <hr /> </div> ); } export default App;
Now we’ll build the non-existent SimpleCountdown
component:
import React, { useState, useEffect, useRef } from "react"; const SimpleCountdown = ({ date }) => { /* Need to calculate something from the date value which will give these variables `isValidDate` - False if not valid, True if valid date `isValidFutureDate` - False if its a past date, True if valid future date `timeLeft` - An object which updates every second automatically to give you the number of days, hours, minutes and seconds remaining. */ const isValidDate = false, isValidFutureDate = false, timeLeft = {}; // The UI just displays what we computed using the date value we received so that return ( <div className="countdown"> <h3 className="header">Simple Countdown</h3> {!isValidDate && <div>Pass in a valid date props</div>} {!isValidFutureDate && ( <div> Time up, let's pass a future date to procrastinate more{" "} <span role="img" aria-label="sunglass-emoji"> 😎 </span> </div> )} {isValidDate && isValidFutureDate && ( <div> {timeLeft.days} days, {timeLeft.hours} hours, {timeLeft.minutes}{" "} minutes, {timeLeft.seconds} seconds </div> )} </div> ); }; export default SimpleCountdown;
The above example just shows a UI example.
Using the date
props, we need to compute these three values. One of the object variables is computed and updated every second.
In React, it’s a state that automatically updates every second.
isValidDate
– false if not valid, true if it’s the valid date
isValidFutureDate
– false if it’s a past date, true if it’s the valid future date
timeLeft
– an object which updates every second automatically to give you the number of days, hours, minutes, and seconds remaining.
Let’s knock off the easy stuff and then calculate all these values from the date:
// To check the date, we are using date-fns library import isValid from "date-fns/isValid"; // This function calc the time remaining from the date and also check whether the date is a valid future date export const calculateTimeLeft = date => { // Check valid date, if not valid, then return null if (!isValid(date)) return null; // Get the difference between current date and date props const difference = new Date(date) - new Date(); let timeLeft = {}; // If there is no difference, return empty object. i.e., the date is not a future date if (difference > 0) { // if there is a differece, then calculate days, hours, minutes and seconds timeLeft = { days: Math.floor(difference / (1000 * 60 * 60 * 24)), hours: Math.floor((difference / (1000 * 60 * 60)) % 24), minutes: Math.floor((difference / 1000 / 60) % 60), seconds: Math.floor((difference / 1000) % 60) }; } // Return the timeLeft object return timeLeft; };
Let’s put this function in a separate utils.js
file and import it into our component file:
// simple-countdown.js import React, { useState, useEffect, useRef } from "react"; // import our util function which calculate the time remaining import { calculateTimeLeft } from "../utils"; const SimpleCountdown = ({ date }) => { // Calculate the initial time left const initialTimeLeft = calculateTimeLeft(date); // assign it to a state, so that we will update the state every second const [timeLeft, setTimeLeft] = useState(initialTimeLeft); const timer = useRef(); // Inorder to update the state every second, we are using useEffect useEffect(() => { // Every second this setInterval runs and recalculate the current time left and update the counter in the UI timer.current = setInterval(() => { setTimeLeft(calculateTimeLeft(date)); }, 1000); // Cleaning up the timer when unmounting return () => { if (timer.current !== undefined) { clearInterval(timer.current); } }; }, [date]); let isValidDate = true, isValidFutureDate = true; // If timeLeft is Null, then it is not a valid date if (timeLeft === null) isValidDate = false; // if timeleft is not null but the object doesn't have any key or seconds key is undefined, then its not a future date if (timeLeft && timeLeft.seconds === undefined) isValidFutureDate = false; // Return the UI return ( .... ); }; export default SimpleCountdown;
It’s very simple.
First, we calculate the initial time left and then assign it to a state. Then we create a setInterval
to update the state every second and recalculate the time left.
That way, it recalculates the time left every second and updates the UI like a countdown timer.
We have successfully created a nice, simple UI using our functionality. As you can see, all our functionalities are isolated from the UI.
Still, the UI resides inside the SimpleCountdown
component.
If you want to create another countdown UI with SVG and CSS animations, then you need to create a new component. If you want to avoid that, extract out the functionality and just make the UI dumb and separated.
Let’s separate the UI into separate files and create multiple versions of it:
// 1st version of React countdown UI import React from "react"; const FirstCountdownUI = ({ timeLeft, isValidDate, isValidFutureDate }) => { return ( <div className="countdown"> <h3 className="header">First Countdown UI</h3> {!isValidDate && <div>Pass in a valid date props</div>} {!isValidFutureDate && ( <div> Time up, let's pass a future date to procrastinate more{" "} <span role="img" aria-label="sunglass-emoji"> 😎 </span> </div> )} {isValidDate && isValidFutureDate && ( <div> <strong className="countdown-header">{timeLeft.days}</strong> days,{" "} <strong className="countdown-header">{timeLeft.hours}</strong> hours,{" "} <strong className="countdown-header">{timeLeft.minutes}</strong>{" "} minutes,{" "} <strong className="countdown-header">{timeLeft.seconds}</strong>{" "} seconds </div> )} </div> ); }; export default FirstCountdownUI;
// 2nd version of React countdown UI import React from "react"; const SecondCountdownUI = ({ timeLeft, isValidDate, isValidFutureDate }) => { return ( <div className="countdown"> <h3 className="header">Second Countdown UI</h3> {!isValidDate && <div>Pass in a valid date props</div>} {!isValidFutureDate && ( <div> Time up, let's pass a future date to procrastinate more{" "} <span role="img" aria-label="sunglass-emoji"> 😎 </span> </div> )} {isValidDate && isValidFutureDate && ( <div> <strong className="countdown-header">{timeLeft.days} : </strong> <strong className="countdown-header"> {timeLeft.hours} :{" "} </strong> <strong className="countdown-header"> {timeLeft.minutes} :{" "} </strong> <strong className="countdown-header">{timeLeft.seconds}</strong> </div> )} </div> ); }; export default SecondCountdownUI;
We have created two different UI. Now we will create the headless component so that we can easily reuse the functionality with any of the UI components.
Basically, we are going to reuse the same logic we created and just change the way we render the UI.
import { useState, useEffect, useRef } from "react"; import { calculateTimeLeft } from "../utils"; /* All logic are same as previous implementation. Only change is, Instead of rendering a UI, we just send the render props */ const Countdown = ({ date, children }) => { const initialTimeLeft = calculateTimeLeft(date); const [timeLeft, setTimeLeft] = useState(initialTimeLeft); const timer = useRef(); useEffect(() => { timer.current = setInterval(() => { setTimeLeft(calculateTimeLeft(date)); }, 1000); return () => { if (timer.current !== undefined) { clearInterval(timer.current); } }; }, [date]); let isValidDate = true, isValidFutureDate = true; if (timeLeft === null) isValidDate = false; if (timeLeft && timeLeft.seconds === undefined) isValidFutureDate = false; // Instead of rendering a UI, we are returning a function through the children props return children({ isValidDate, isValidFutureDate, timeLeft }); }; export default Countdown;
You can call this as a children prop, as a function, or as a render prop.
Both are one and the same. It doesn’t need to be the children props. It can be any props you can return as a function, and that a parent component can use to ender UI through the variables returned through the render props. This is common way of doing it.
Rendering the UI is simple.
// On Page 1 - We render first countdown UI import React from "react"; import FirstCountdownUI from './first-countdown-ui'; import Countdown from './countdown-render-props'; function App() { const date = new Date("2021-01-01"); // New year! return ( <Countdown date={date}> {(renderProps) => ( <FirstCountdownUI {...renderProps} /> )} </Countdown> ); } export default App;
On the second page with React countdown:
// On Page 2, we render second countdown UI import React from "react"; import SecondCountdownUI from './second-countdown-ui'; import Countdown from './countdown-render-props'; function App() { const date = new Date("2021-01-01"); // New year! return ( {(renderProps) => ( )} ); } export default App;
This way, you can reuse the functionality and create multiple different UI with the same functional component.
This same headless component can be achieved using custom Hooks as well. Doing it this way is less verbose than doing it with render props-based components.
Let’s do that in our next step:
First, we will build the custom Hook, which will provide the timeLeft
, isValidDate
and isvalidFutureDate
variables.
// use-countdown.js - custom hooks import { useState, useEffect, useRef } from "react"; import { calculateTimeLeft } from "../utils"; // All the computation are same as previous, only change is, we directly return the values instead of rendering anything. const useCountdown = date => { const initialTimeLeft = calculateTimeLeft(date); const [timeLeft, setTimeLeft] = useState(initialTimeLeft); const timer = useRef(); useEffect(() => { timer.current = setInterval(() => { setTimeLeft(calculateTimeLeft(date)); }, 1000); return () => { if (timer.current !== undefined) { clearInterval(timer.current); } }; }, [date]); let isValidDate = true, isValidFutureDate = true; if (timeLeft === null) isValidDate = false; if (timeLeft && timeLeft.seconds === undefined) isValidFutureDate = false; // We return these computed values for the passed date prop to our hook return { isValidDate, isValidFutureDate, timeLeft }; }; export default useCountdown;
This Hook will abstract everything, compute the timeLeft
every second, and return it to the component, which is going to use this Hook.
Let’s render our 2 pages with 2 different UI and the same custom countdown Hook:
// On Page 1 - We render first countdown UI import React from "react"; import FirstCountdownUI from './first-countdown-ui'; import useCountdown from './use-countdown'; // importing the custom hook function App() { const date = new Date("2021-01-01"); // New year! // pass in the date and get all the values from the hook, throw it to the UI const { timeLeft, isValidDate, isValidFutureDate } = useCountdown(date); return ( <FirstCountdownUI timeLeft={timeLeft} isValidDate={isValidDate} isValidFutureDate={isValidFutureDate} /> ); } export default App;
On the second page with the custom countdown Hook:
// On Page 2, we render second countdown UI import React from "react"; import SecondCountdownUI from './second-countdown-ui'; import useCountdown from './use-countdown'; // importing the custom hook function App() { const date = new Date("2021-01-01"); // New year! // pass in the date and get all the values from the hook, throw it to the UI const { timeLeft, isValidDate, isValidFutureDate } = useCountdown(date); return ( <SecondCountdownUI timeLeft={timeLeft} isValidDate={isValidDate} isValidFutureDate={isValidFutureDate} /> ); } export default App;
With this method, we can reuse the components and separate logic from the UI.
You can even publish this headless component as an NPM library separately and use it in multiple projects.
Some heavily-used headless components in the React world include:
You can checkout those code bases to learn a ton and see how elegantly these libraries are made.
Hope you learned some tricks in React.
You can checkout the example codebase here, and you can checkout the demo here.
Share your thoughts in the comments.
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 nowSimplify component interaction and dynamic theming in Vue 3 with defineExpose and for better control and flexibility.
Explore how to integrate TypeScript into a Node.js and Express application, leveraging ts-node, nodemon, and TypeScript path aliases.
es-toolkit is a lightweight, efficient JavaScript utility library, ideal as a modern Lodash alternative for smaller bundles.
The use cases for the ResizeObserver API may not be immediately obvious, so let’s take a look at a few practical examples.
One Reply to "The complete guide to building headless interface components in React"
great job , very helpful , thanks