DEV Community

Cover image for The complete guide to building headless interface components in React
Brian Neville-O'Neill
Brian Neville-O'Neill

Posted on • Originally published at blog.logrocket.com on

The complete guide to building headless interface components in React

Written by Paramanantham Harrison✏️

Introduction

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.

What are headless 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:

  • sort by column
  • search using free text on the table
  • inline editable row

There are two options for building this kind of component.

Building a smart 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:

  • build a separate component without reusing any logic
  • build a headless component by reusing just a functionality

How? I’ll explain.

LogRocket Free Trial Banner

Headless component

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.

When do you need headless components

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.

When headless components are overkill

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.

Building React countdown components with simple UI

Specs for our React-dropdown component:

  • For a future date, it will render a countdown with days, hours, minutes, and seconds remaining for that date.
  • For an old date or a non-date, it will show a relevant error message.

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
};
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode
// 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;
Enter fullscreen mode Exit fullscreen mode

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.

Headless component using render props

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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:

Custom React Hooks (headless components)

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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.

Conclusion

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.


Full visibility into production React apps

Debugging React applications can be difficult, especially when users experience issues that are difficult to reproduce. If you’re interested in monitoring and tracking Redux state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.

Alt Text

LogRocket is like a DVR for web apps, recording literally everything that happens on your React app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app's performance, reporting with metrics like client CPU load, client memory usage, and more.

The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.

Modernize how you debug your React apps — start monitoring for free.


The post The complete guide to building headless interface components in React appeared first on LogRocket Blog.

Top comments (0)