Building and publishing your first NPM package

Custom Hook: creating, testing and publishing it to npm and beyond

You have created a new reusable piece of code and want to share it with everyone or maybe you just have an idea that can be useful in different projects. But you are completely lost about how to start to code and create a npm package or even how to publish the code you already have.

I've been there, I created some small packages such as ICollections, Ngx-indexed-db, React-indexed-db and now I want to help you to create and publish your first package. This tutorial will be exclusively focused how to create a simple package, I will not be covering several things a project can benefit from, such as use of TypeScript, semantic-release, CI, and etc.

We will build together a custom hook for React that can be very useful in daily basis, a simple toggle state. If you are not familiar with React Hooks, check this link: React Hooks Docs.

The idea is to be able to install the package via NPM running

npm install useToggle

And then use it in any project as in the code bellow:

import React from 'react';
import useToggle from 'useToggle';

const App = () => {
  const [isLoading, toggleLoading] = useToggle(true);
  return (
    <div>
      <button onClick={toggleLoading}>Toggle</button>
      {isLoading ? <div>loading...</div> : <div>Content</div>}
    </div>
  );
};

export default App;

Let’s start creating a folder I will name useToggle, navigating to inside the folder and initializing it as a npm package.

Run the following commands in your console:

mkdir useToggle // to create the folder
cd useToggle // to navigate inside the folder
npm init // to initialize the the npm inside the folder

When we run npm init we have to answer some questions that should be straightforward. Here is my final result for this last command:

{
  "name": "usetoggle",
  "version": "1.0.0",
  "description": "React hook to facilitate the state toggle",
  "main": "lib/index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [
    "react",
    "hooks",
    "toggle"
  ],
  "author": "Charles Assuncao",
  "license": "ISC"
}

Installing the dependencies

We are going to need some things to create the project, let’s install it via npm:

We are going to need useState from react for the package to work, so let’s install it as normal dependency

npm install react

We are going to use babel here to transpile and minify the final code:

npm install --save-dev @babel/core  @babel/cli  @babel/preset-env babel-preset-minify

Notice that this time we passed the flag --save-dev to signalize that this dependency are only needed to develop our code but it’s not a dependency to the package to work.

We want to test our code and be sure everything works as expected, remember: not tested code is broken code! Since we are creating a custom hook we will need React Hooks Testing Library

npm install --save-dev jest @testing-library/react-hooks react-test-renderer

Hands-on, Let’s code!

Writing tests

Let’s start writing tests and caring more how we expect our code works. Test Driven Development has several advantages and I definitely recommend a deeper reading about it.

Create the folder where we are going to keep our code:

mkdir src

Create three new files inside this folder:

index.js

useToggle.js

useToggle.spec.js

Our project now looks basically like this:

├── package-lock.json
├── package.json
├── node_modules
├── src
│   ├── index.js
│   ├── useToggle.js
│   ├── useToggle.spec.js

Since we installed jest to run our tests we need now to create a test script in our package.json

"scripts": {
    "test": "jest"
}

I love this simplicity of jest, needless configuration. Now we are able to run npm run test to execute our specs files. Let’s create our first test then:

//useToggle.spec.js

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

describe('useToggle Hook', () => {
  test('Should initiate with false as default', () => {
    const { result } = renderHook(() => useToggle());
    expect(result.current[0]).toBe(false);
  });
});

What is happening here?

We create a test suit for our Hook ‘useToggle Hook’ and our first test is to check the default value initialized in our hook. renderHook execute our hook and return an object that contains the hooks returned value inside of result.current. In our case our hook will return an array with the state value and a function to mutate the state. So basically:

result.current[0] // is our state, by default false
result.current[1] // is the toggleState function

If we run npm run test now our tests will be red. Because we don’t have anything inside the useToggle.js file. So let’s create a simple function that will make our test turn green:

//useToggle.js

export default function useToggle(initialState = false) {
  return [initialState];
}

Run the tests now and see it green as the happiness 

Our function is already returning array having the default initial value as false. Let’s think and create some more tests of how we expect our hook to work:

//useToggle.spec.js
import { renderHook, act } from '@testing-library/react-hooks';
import useToggle from './useToggle';

describe('useToggle Hook', () => {
  test('Should initiate with false as default', () => {
    const { result } = renderHook(() => useToggle());
    expect(result.current[0]).toBe(false);
  });

  test('Should initiate with the provided value', () => {
    const { result } = renderHook(() => useToggle(true));
    expect(result.current[0]).toBe(true);
  });

  test('Should toggle the value from false to true', () => {
    const { result } = renderHook(() => useToggle());
    act(() => {
      result.current[1]();
    });
    expect(result.current[0]).toBe(true);
  });
});

The first two tests will pass, our useToggle function is returning an mock of the state that fulfill the requirements for the two initial tests. But our hook doesn’t make anything happen actually. So let’s change this and make our tests run green again.

import { useState } from 'react';

export default function useToggle(initialState = false) {
  const [state, setState] = useState(initialState);

  function toggleState() {
    setState(!state);
  }

  return [state, toggleState];
}

We imported useState from react and we are using it to hold our initial value and to change it through the function setState but instead of returning the setState function we create a closure that toggles the state value behaving as we expected.

Run the tests now and see your console sparkling joy with all tests passing. But, let’s create some more tests just for fun. The final test file will be something like this:

import { renderHook, act } from '@testing-library/react-hooks';
import useToggle from './useToggle';

describe('useToggle Hook', () => {
  test('Should initiate with false as default', () => {
    const { result } = renderHook(() => useToggle());
    expect(result.current[0]).toBe(false);
  });

  test('Should initiate with the provided value', () => {
    const { result } = renderHook(() => useToggle(true));
    expect(result.current[0]).toBe(true);
  });

  test('Should toggle the value from false to true', () => {
    const { result } = renderHook(() => useToggle());
    act(() => {
      result.current[1]();
    });
    expect(result.current[0]).toBe(true);
  });

  test('Should toggle the value from true to false', () => {
    const { result } = renderHook(() => useToggle(true));
    act(() => {
      result.current[1]();
    });
    expect(result.current[0]).toBe(false);
  });

  test('Should execute multiple toggles', () => {
    const { result } = renderHook(() => useToggle()); //init false
    // false -> true
    act(() => {
      result.current[1]();
    });
    // true -> false
    act(() => {
      result.current[1]();
    });
    // false -> true
    act(() => {
      result.current[1]();
    });
    // true -> false
    act(() => {
      result.current[1]();
    });
    expect(result.current[0]).toBe(false);
  });
});

Last but not least, we should export our hook from our entry point index.js. Only one line will do the job:

// index.js

export { default } from './useToggle';

Building

Let’s configure the build script, we are going to need babel for that. So let’s create a babel configuration file (babel.config.js). Our configuration should be damn simple:

//babel.config.js

module.exports = {
  presets: ['@babel/preset-env', 'minify'],
};

And create a build a build script inside our package.json:

"scripts": {
    "test": "jest",
    "build": "babel src --out-dir lib"
}

Now we can run npm run build and it will generate the lib/index.js file.

Publish

We need to make small changes in our package.json in order to publish it. Let’s configure the files that should be included in the package and a special script that will run every time when we try to publish the package. Also, we are going to change the react dependency to be a peerDependency, since we are expecting the project using our package already has its own react version:

"files": [
    "lib"
  ],
  "scripts": {
    ...
    "prepublish": "npm run build"
  }, 
  . . .
  "peerDependencies": {
    "react": "^16.9.0"
  },

Run npm login and use the credentials that we created in the npm site previously. After successfully login you can run now npm publish. Now your package lives in the wild world of npm packages and can be used for anyone only running npm install useToggle

Comentários