DEV Community 👩‍💻👨‍💻

Cover image for 10 Things I Learned by Making a React Hook Library
Gabriel Abud
Gabriel Abud

Posted on

10 Things I Learned by Making a React Hook Library

Since the introduction of Hooks in React 16.8, there has been an explosion of Hook libraries, and for good reason. Hooks are how you reuse code in functional components. Without Hooks, functional components would not be a viable alternative to class based components.

While I had made custom Hooks before, I had procrastinated making my own React Hook library. I knew if I made my own custom Hook without making it a library, it would die in the uncompleted side project graveyard that is my Github. So I needed to make it shareable in order to hold me accountable.

What I Built

I was frustrated by the existing table libraries out there. In most of my projects, I need to show tabular data in one form or another. While most existing table libraries do a decent job, once you start deviating from the default UI you end up fighting with the library a lot. These table libraries have a ton of existing issues, because it's hard to present a UI that is truly flexible to all the use cases.

What I needed was an easy way to handle the table state with no opinions on the UI. I stumbled on to the idea of headless components, which seemed like the perfect fit for what I needed. In essence, a headless component provides you the tools to manage the state of the component, without giving you an opinionated UI.

Introducing React Final Table

Building off of this concept, I came up with React Final Table. It's an extremely lightweight (1.5KB minzipped and 0 dependencies), type-safe, headless component library that exposes a single hook. Out of the box, it supports filtering, searching, selection, sorting, and pagination for your tables. In its most basic form, it looks like this:

import { useTable } from 'react-final-table';

const columns = [
  {
    name: 'firstName',
    label: 'First Name',
    render: ({ value }) => <h1>{value}</h1>,
  },
  {
    name: 'lastName',
    label: 'Last Name',
  },
];

const data = [
  {
    firstName: 'Frodo',
    lastName: 'Baggins',
  },
  {
    firstName: 'Samwise',
    lastName: 'Gamgee',
  },
];

const MyTable = () => {
  const { headers, rows } = useTable(columns, data);

  return (
    <table>
      <thead>
        <tr>
          {headers.map((header, idx) => (
            <th key={idx}>{header.render()}</th>
          ))}
        </tr>
      </thead>
      <tbody>
        {rows.map((row, idx) => (
          <tr key={idx}>
            {row.cells.map((cell, idx) => (
              <td key={idx}>{cell.render()}</td>
            ))}
          </tr>
        ))}
      </tbody>
    </table>
  );
};
Enter fullscreen mode Exit fullscreen mode

See the documentation for more example use cases.

Without further ado, here are the top things I learned during this process:

1. React Hooks are great

The React equivalent to reusable functions are Hooks. Anywhere that you are duplicating non-UI code across components, you should evaluate whether making a custom Hook is worthwhile. Some of the most likely candidates are authentication and fetching data.

2. TSDX makes development a breeze

TSDX is a library for quick development of Typescript packages. There is a template for React, which means getting started with a new package takes only a few seconds. See my guide on using TSDX for more.

3. React Testing Library makes testing a breeze

While I'm a big fan of testing in Python, Jest, and even end to end testing with Cypress, I wasn't sure how to best do integration testing on the frontend. I knew I didn't want to break everything up into unit tests, since what I really want to test is that it works as expected, regardless of the implementation details.

React Testing Library makes it easy to test React Hooks without testing the implementation. This means my tests resemble how people are likely to use my library without being brittle to changes in the codebase.

4. Use sandbox examples

While testing is great, it can only get you so far. Sometimes you need to visually see what's happening as you add features. For this, it's important to set up a couple of sandbox examples that use your unpublished library. You can use npm link (or yarn link) to link to your unpublished library. Once you've published a version, you can share Sandbox examples using a link with Codesandbox.io. Here's an example of React Final Table using Codesandbox.

Or better yet, set up Storybook with many example use cases. Storybook allows you to develop many components in isolation along with documentation.

5. There is more to Hooks than useState and useEffect

While we all start out overusing Hooks like useState and useEffect, there are many others to be aware of.

Some of my favorite lesser known Hooks are useReducer (for complex state), useMemo/useCallback (for performance), and useRef (for persisting changes outside of the render lifecycle). In this library I made heavy use of all of these.

6. Headless UI components > UI components

From personal experience, headless UI components are more resilient to change and easier to refactor. It might seem convenient to use a pre-made component library with a UI, but think twice if you're going to be customizing it a lot. It might end up creating more work than you anticipated.

7. Use Typescript Generics for flexible libraries

In order to make a flexible Typescript library, I had to be sure I let people store any type of data in their tables. But how can you do this safely without using type any everywhere?

This is where Generics come in. They let us specify the type at runtime and still ensure that Typescript is performing type safety checks. Here is a code snippet to elaborate on what a Generic is:

// this function takes an element of any type and returns that same type
function identity<T>(arg: T): T {
  return arg;
}

console.log(typeof identity(42)); // number
console.log(typeof identity('string')); // string
console.log(typeof identity(undefined)); // undefined
Enter fullscreen mode Exit fullscreen mode

8. Github Actions and continuous integration are essential

If you're not using continuous integration in your personal or work projects, you should definitely start. I used to be a heavy CircleCI user, but recently I've started to prefer Github Actions. The configuration and integration in Actions is much simpler than CircleCI, and the pre-built action ecosystem is much larger. In React Final Table, besides the ESLint, Prettier, and testing steps, I've added a Github Action to run code coverage and another to publish to NPM automatically, based on commit messages. This has greatly simplified my development process, as I don't need to manually update the version and publish to npm every time I make a change.

9. Learn from open source

I often get ideas around architecture, best practices, and contributing practices from open source projects I like. Some notable ones I found helpful for this library were downshift(another headless UI library), react-hook-form, and react-query.

10. Solve your own problems

The last recommendation I have is to make libraries that will solve a problem you have. Nothing is more demotivating than creating something you have no need for, so be sure you will be one of the main users of this library.

By creating things that you need, you will also have a vested interest in your library and are more likely to produce something of higher value.

Top comments (0)

🤔 Did you know?

 
🌚 Dark mode is available in Settings.