A TypeScript tale - How to publish a Custom Hook on NPM with TypeScript

By Francisco Gomes23 Aug 2020
Reading time: 7 mins

Find out how to publish a React Custom Hook on NPM using TypeScript

On this page:

What is NPM?

NPM stands for Node Package Manager and is the world’s largest software registry. NPM is used to share, borrow, develop packages that can be public or private to your organization. It’s also a command-line utility for interacting with packages and it’s hard to imagine a world without NPM.

Why React Custom Hooks?

As sharp and conscious developers that we all are, we find ourselves many times following the DRY (Don’t Repeat Yourself) principles, therefore building reusable pieces of code within our project. If we’re diving deep in React Hooks that share the same capabilities with cross functionalities, we should be taking advantage of Custom Hooks. Hooks, in nature, are plain JavaScript functions.

“Building your own Hooks lets you extract component logic into reusable functions.” - React documentation.

Where does TypeScript fit in the equation?

TypeScript is a superset of JavaScript and it’s created and maintained by Microsoft. As a language on its own, it has been designed to offer JavaScript the typing system it was missing. It provides compile-time type validation and will not allow our code to compile if there are any typing errors. It has a huge community support not only in the forms of “how-to”s and documentation but also with declaration files.

The motivation

Recently, at work, we’ve built a form that was making use of validations. Since the company I work at, has dozens of other React projects, our team thought that it could be useful to publish the Custom Hook as an NPM package so other teams could make use of it as well. Since the package is fully tested and maintained by us, other teams can enjoy the benefits of a package on these terms without having to worry about maintenance. As a plus, developers will be able to generate value in the form of pull-requests and contributions.

Putting all together with TSDX

I started by building the backbone, adding prettier, ESLint, TypeScript, Rollup, Jest and an example app that would make use of the Custom Hook. It was not as simple as I thought and I stepped on many rocks before I could even start typing any code. My goal was not to spend hours or days on the configuration. I wanted to be reliable, but able to move fast.

I then found TSDX, “a zero-config CLI that helps you develop, test, and publish modern TypeScript packages with ease--so you can focus on your awesome new library and not waste another afternoon on the configuration.”. It couldn’t sound better.

TSDX is another product from the team that built Formik and has dozens of contributors. Some of the shining features are:

  • Bundles your code with Rollup and outputs multiple module formats (CJS & ESM by default, and also UMD if you want) plus development and production builds
  • Live reload / watch-mode
  • Works with React
  • Jest test runner setup with sensible defaults via tsdx test
  • Zero-config, single dependency

Let’s try it then!

Node and NPM

Make sure you’ve got Node and NPM latest versions installed on your machine.

Choose a name for your package

Picking a name can be burdensome, especially when it comes to Custom Hooks. As React docs say: “A custom Hook is a JavaScript function whose name starts with ”use” and that may call other Hooks.”. If you wanna build something like a “useCounter”, you’ll probably find a dozen of them already registered on https://www.npmjs.com/. I’d suggest something like “@<company/user name>/useCounter”.

But we are not going to build a useCounter. There are way too many in this world and there are other things that are not getting enough attention.

We are going to use the Chuck Norris API because life it’s better when you know some Chuck Norris facts.

The goal of this tutorial is not to understand how to build a Custom Hook. If you’re not sure about it, have a look at the official documentation.

Run TSDX

On your terminal/command line, type the following:

npx tsdx create use-norris

The terminal will prompt a question and we’ll choose “react”

Dependencies will be generated and the boilerplate created.

To start developing:

```bash

cd use-norris

yarn

```

Our project file tree will look like this:

Things to note

package.json

I’m ok with the `package.json` for now but I’ll change the `name` value for what will be my package name to `@franciscomcg/use-norris`. Like so:

“name”: “@franciscomcg/use-norris”

Publishing on NPM and creating a git repository are separated tasks and one has nothing to do with another. We can publish and jump git completely but, as we know, is not a great idea. Therefore I’ll add my git repository in the `package.json`:

"repository": {
   "type": "git",
   "url": "https://github.com/FranciscoMCG/use-norris"
 },

tsconfig.json

All good for me but I’d like you to pay special attention to the `”declaration”: “true”` field. This field would make it possible to generate corresponding declaration (`*.d.ts`) files for your code and ship them with our package. This would be used by our users to access type definitions.

ESLint and Prettier

The package runs with ESLint and Prettier out of the box. You can customize both by adding `eslintConfig` and `prettier` blocks to `package.json` or by creating `.eslintrc.js` and `.prettierrc.js` config files.

Building our Hook

Our Hook is going to be very straight forward, and it’s only functionality is to be able to randomly fetch Chuck Norris facts.

On our `src/index.tsx` we will replace the code for the following:

Initial config:

import { useEffect, useReducer } from 'react';

interface InitialState {
 response: any;
 isLoading: boolean;
 isError: boolean;
 errorMessage: string | null;
}

enum ActionType {
 FETCH_INIT = 'FETCH_INIT',
 FETCH_SUCCESS = 'FETCH_SUCCESS',
 FETCH_FAILURE = 'FETCH_FAILURE',
 DATA_NOT_FOUND = 'DATA_NOT_FOUND',
}

const { FETCH_FAILURE, FETCH_INIT, FETCH_SUCCESS, DATA_NOT_FOUND } = ActionType;

The actions types and initial state:

type UseNorrisAction =
 | { type: ActionType.FETCH_SUCCESS; payload: string }
 | {
     type: ActionType.FETCH_FAILURE;
     payload: { isError: boolean; errorMessage: string | null };
   }
 | { type: ActionType.FETCH_INIT }
 | { type: ActionType.DATA_NOT_FOUND; payload: string };

const initialState: InitialState = {
 response: '',
 errorMessage: null,
 isLoading: false,
 isError: false,
};

The reducer:

const useNorrisReducer = (
 state: InitialState = initialState,
 action: UseNorrisAction
) => {
 switch (action.type) {
   case FETCH_INIT:
     return {
       ...state,
       isLoading: true,
       isError: false,
     };
   case FETCH_SUCCESS:
     return {
       ...state,
       response: action.payload,
       isLoading: false,
       isError: false,
     };
   case FETCH_FAILURE:
     return {
       ...state,
       isLoading: false,
       isError: action.payload.isError,
       errorMessage: action.payload.errorMessage,
     };
   case DATA_NOT_FOUND:
     return {
       ...state,
       isLoading: false,
       isError: true,
       errorMessage: action.payload,
     };
   default:
     return { ...state };
 }
};

The hook:

const useNorris = (initialState: InitialState) => {
 const [state, dispatch] = useReducer(useNorrisReducer, initialState);
 const { response, errorMessage, isError, isLoading } = state;

 useEffect(() => {
   const fetchNorris = async () => {
     dispatch({ type: FETCH_INIT });
     try {
       const res = await fetch('https://api.chucknorris.io/jokes/random');
       const json = await res.json();

       if (json.error) {
         dispatch({ type: DATA_NOT_FOUND, payload: json.error });
       }
       if (json.value) {
         dispatch({ type: FETCH_SUCCESS, payload: json });
       }
     } catch (error) {
       dispatch({
         type: FETCH_FAILURE,
         payload: { isError: true, errorMessage: error },
       });
     }
   };
   fetchNorris();
 }, []);

 return { response, errorMessage, isLoading, isError };
};

export default useNorris;

Testing our Hook

Testing Custom Hooks can be a daunting task. Normally we’d use a React component to trigger the hook's various functionalities. But it happens that we don’t have a component. So how will we tackle this problem? We’ll use react-hooks-testing-library.

“Allows you to create a simple test harness for React hooks that handles running them within the body of a function component, as well as providing various useful utility functions for updating the inputs and retrieving the outputs of your amazing custom hook.”

By using this library we avoid rendering the React component to test our hook.

Install it as a dev dependency:

(We’ll need also react-test-renderer since it is a peer-dependency and the types for the libraries).

yarn add -D @testing-library/react-hooks react-test-renderer @types/jest @testing-library/react-hooks @testing-library/jest-dom

Now that we have react-hooks-testing-library installed, we’ll rename the file in the `test` folder to `useNorris.test.ts` and replace the code with the following:

import { renderHook } from '@testing-library/react-hooks';

import useNorris from '../';

const mockedValue = {
 value: 'this is a very good joke',
};

const initialState = {
 response: { value: '' },
 isLoading: false,
 errorMessage: '',
 isError: false,
};

(global.fetch as jest.Mock) = jest.fn(() =>
 Promise.resolve({
   json: () => Promise.resolve(mockedValue),
 })
);

describe('useNorris', () => {
 beforeEach(() => {
   (fetch as jest.Mock).mockClear();
 });

 it('should resolve', async () => {
   const { result, waitForNextUpdate } = renderHook(() =>
     useNorris(initialState)
   );
   await waitForNextUpdate();
   expect(fetch).toHaveBeenCalled();
   expect(result.current).toEqual({
     response: mockedValue,
     errorMessage: '',
     isLoading: false,
     isError: false,
   });
 });

 it('should return an error', async () => {
   (fetch as jest.Mock).mockImplementationOnce(() =>
     Promise.reject('There is an error')
   );
   const { result, waitForNextUpdate } = renderHook(() =>
     useNorris({
       response: { value: '' },
       isLoading: false,
       errorMessage: '',
       isError: false,
     })
   );
   await waitForNextUpdate();
   expect(fetch).toHaveBeenCalled();
   expect(result.current).toEqual({
     errorMessage: 'There is an error',
     isLoading: false,
     response: { value: '' },
     isError: true,
   });
 });
});

The example app

We wrote a basic test and we should build a usage example so users are able to understand it better.

In the example folder our `./index.ts` will look like this:

import * as React from 'react';

import useNorris from '../../src';

const App = () => {
 const initialState = {
   response: '',
   isLoading: false,
   isError: false,
   errorMessage: null,
 };
 const { response, isLoading, isError, errorMessage } = useNorris(
   initialState
 );

 if (errorMessage) {
   return <p>{errorMessage}</p>;
 }

 if (isError) {
   return <p>Something went wrong</p>;
 }

 if (isLoading) {
   return <p>Loading...</p>;
 }

 if (response) {
   return <p>{response.value}</p>;
 }
 return <p>Something went wrong</p>;
};

export default App;

Go to the `example` folder and type:

yarn start

This would start the server and if we go to `localhost:1234` we’ve got our app running!

Add a README file

Writing a good, concise and meaningful README file is super important. We want our users to enjoy the whole set of features we are providing and examining the code, line by line, it’s not the best use of their time.

Publishing our package

With TSDX, publishing is a simple and quick process:

1. Register on NPM

2. Running `yarn publish --access public` will bundle our package to the dist folder and will try to access the NPM registry to publish our Hook. Private packages are a paid feature on NPM and we want our Hook to be available to everyone, hence the `--access public` flag.

3. Following the cli instructions, we’ll need to log in on NPM and type the version we want to use.

And that’s it, we receive a confirmation email and our amazing useNorris is now available to everyone.

Versioning

It’s worth noticing that versioning plays an important role in how we update packages. Following the correct semantic versioning will ensure other developers are able to know the extent of changes between versions and adjust if necessary. NPM semantic versioning will give you a good idea about this topic.

Conclusions

In this tutorial we’ve built a Custom Hook with TypeScript that fetches random Chuck Norris jokes. We’ve published it on NPM and now other developers will have the chance to use it as well.

Resources

- Final repo - https://github.com/FranciscoMCG/use-norris

- NPM package - https://www.npmjs.com/package/@franciscomcg/use-norris

- TSDX - https://github.com/formium/tsdx

- Chuck Norris API - https://api.chucknorris.io

- React Hooks Testing Library - https://github.com/testing-library/react-hooks-testing-library

Share this on:


Comments? Shoot me a tweet @reactgqlacademy !

Help us help you learn :)

What are your main interests in a training?

Using our site means you consent to our use of cookies. Find out more in our privacy policy.