In this tutorial, we'll go over how to create a simple custom React hook, testing it locally, and then publishing it on NPM. The React hook we'll create is useOnline which detects if the user goes offline and shows them a message that they're offline.

After implementing it, we'll check how we can test it locally, then publishing it on NPM.

If you're checking out this tutorial to learn only how to create a custom hook to use it in an existing project without intending on publishing it as a package on NPM, then you can stop before the testing and publishing part of this tutorial. You probably also won't need to go through the Setup part as well.

The code for this tutorial is available on this GitHub Repository.


What are Custom Hooks?

Custom hooks hold a certain logic that make use of React's hooks like useState, useEffect, etc... You usually create custom hooks when a certain part in your project is reusable and makes use of React's hooks. So, you create a custom hook that you can use throughout your project just like you would use React's hooks. It should also start with use.


Setup

Let's start by creating a new directory and changing to it:

mkdir use-online
cd use-online

Then, we'll initialize our NPM project:

npm init

You'll have to enter some information that will go into package.json like package name, description, author, main entry, etc... You can use the default settings for now.

Once you're done, you'll have an empty NPM package at your hand. Let's now install the dependencies we'll be using to develop our custom React hook:

npm i --save-dev react @babel/cli copyfiles

We're installing React since we are developing a custom hook. We're also installing babel's CLI to build our code later on, and we're installing copyfiles which we will use later as well when we are getting our package ready for publishing.

Once we're done with that, we're ready to implement our custom hook.


Implementing useOnline

As I mentioned in the beginning, useOnline will detect whenever the user is online or offline. This means that it will manage a state for the user's connectivity status, and listen to any changes in the user's connectivity and update it accordingly.

So, useOnline will make use of useStatus to keep track of the user's connectivity, and will use useEffect to register event listeners for the events online and offline to set the state accordingly. In the end, useOnline will just return the state which we can use in other components to track the user's connectivity without repeating the logic behind it.

Let's start by creating the file that will hold our custom hook. Create src/useOnline.js with the following content:

import { useState, useEffect } from 'react'

function useOnline () {

}

export default useOnline

We're just importing useState and useEffect to use them in a bit, declaring the custom hook useOnline and exporting it.

Now, let's get to the code of the hook. First, let's create the state that will hold the user's connectivity:

function useOnline () {
    const [online, setOnline] = useState(navigator.onLine);
    
}

online will hold the state of the user's connectivity and it will be a boolean. If the user is online it will be true, if not it will be false. For its initial value, we are using the value of navigator.onLine which returns the online status of the browser.

Next, we need to listen to the online and offline events. The online event occurs when the user goes online, and the offline event occurs when the user goes offline. To add the listeners, we will use useEffect:

function useOnline () {
	const [online, setOnline] = useState(navigator.onLine)
    
    useEffect (() => {
        window.addEventListener('online', function () {
            //TODO change state to online
        });
        
        window.addEventListener('offline', function () {
            //TODO change state to offline
        });
    }, [])
}

So, we are adding event listeners to the online and offline events inside useEffect callback. We are also passing an empty array as a second parameter for useEffect. This ensures that the callback is only called on mounting the component.

Now, let's add the logic inside each of the listeners. We just need to change the value of online based on the event. To do this, we will use setOnline:

useEffect (() => {
    window.addEventListener('online', function () {
        setOnline(true)
    });

    window.addEventListener('offline', function () {
        setOnline(false)
    });
}, [])

Pretty easy. Our code now adds an event listener to both online and offline events, which change the value of our state online based on the user's connectivity.

When adding event listeners or adding any kind of subscriptions, we need to make sure that we are cleaning up after the component unmounts. To do that, we return a function in useEffect that removes the event listeners on unmount.

Since we will be using removeEventListener to remove the event listeners, which takes the event listener we are moving as a second parameter, let's remove our event listeners to functions that we can reference:

function offlineHandler () {
    setOnline(false)
}

function onlineHandler () {
    setOnline(true)
}

useEffect (() => {
    window.addEventListener('online', onlineHandler)
    window.addEventListener('offline', offlineHandler)

    return () => {
        window.removeEventListener('online', onlineHandler)
        window.removeEventListener('offline', offlineHandler)
    }
}, [])

We moved our event listeners to functions outside useEffect (you can also add them inside instead) and we are passing them as the event listeners in addEventListener and removeEventListener inside useEffect for both the online and offline events.

The last thing we need to do in our custom hook is return the state we are changing. This way we can use this state in other components with all the logic behind it in one place.

So, the full code for useOnline will be:

import { useState, useEffect } from 'react'

function useOnline () {
    const [online, setOnline] = useState(navigator.onLine)

    function offlineHandler () {
        setOnline(false)
    }

    function onlineHandler () {
        setOnline(true)
    }

    useEffect (() => {
        setOnline(navigator.onLine)
        window.addEventListener('online', onlineHandler)
        window.addEventListener('offline', offlineHandler)

        return () => {
            window.removeEventListener('online', onlineHandler)
            window.removeEventListener('offline', offlineHandler)
        }
    }, [])

    return online
}

export default useOnline;

That's it! We created a custom hook that makes use of React hooks like useState and useEffect to determine the user's connectivity.


Preparing the NPM Package

If you want to publish your custom hook on NPM, you need to prepare the package to be published and used. There are certain things that need to be done, especially in package.json.

In the beginning, we installed @babel/cli and copyfiles. This is where we'll put them into use.

Package Information

When you first run npm init you are asked to enter a few information like package name, description, author, version, license, etc... If you've used the default information, or you want to change this information, make sure you change them prior to publishing. You can do that in the package.json file.

Note that the name in package.json is the package name that people will use to install it. So, make sure it's exactly what you want to call it.

Dependencies

When publishing a package, make sure you are listing the dependencies required correctly. If some dependencies are only required during development and are not necessary to install when they are being used, then include them under devDependencies.

In our example, we should have:

"devDependencies": {
	"react": "^17.0.1",
    "@babel/cli": "^7.13.14",
    "copyfiles": "^2.4.1"
  }

Note that the versions might be different in your project but that's fine.

There's one more thing to note: In a React project, only one installation or instance of react is allowed. Meaning that your package shouldn't install React as well when installing it in a project.

So, let's change react to be a peer dependency like this:

"peerDependencies": {
    "react": "^16.8.0 || ^17.0.1"
  },
  "devDependencies": {
    "@babel/cli": "^7.13.14",
    "copyfiles": "^2.4.1"
  }

When adding a dependency in peerDependencies, the react package you are using in your project that will include this package will be used instead of installing a new one. We are also allowing the version to be at least 16.8.0 since that's when React Hooks were introduced.

Scripts

To make sure our package is ready for use, we will add scripts that will build our React custom hook using babel:

"scripts": {
    "prebuild": "npm i",
    "build": "babel src --out-dir dist"
 },

Now, whenever we run build, prebuild will run first to ensure that the dependencies required are installed, then the build script will compile the Javascript files in our src directory (which is useOnline.js) and outputs the result in dist.

main

If we want our package to be used like this:

import useOnline from 'use-online'

Then we need to specify what we are exporting and which file will be used for the import. It's the main file in our package.

In our case, it will be the output of the build script:

"main": "dist/useOnline.js"

files

When publishing a package, by default, it will publish all the files and directories starting from the root directory. This can increase the package's size significantly, especially if there are a lot of redundant files or files that are not necessary for the package to be used.

In our example, if you look at the GitHub Repository,  you can see that there's an example directory. We will get to what that holds later, but a lot of times you might have examples, images, or other files that might be necessary for the package development-wise, but not when it's published.

To decrease the package size and make sure only relevant files are included, we use the files key:

"files": [
    "dist"
 ],

files takes an array that holds all the files or directories that should be included in the package once published. In our case, it will just be the dist directory that will hold our built code.

types

This one is purely optional and I'm using it in its simplest form. You can add a Typescript declaration for your package. To do so, we'll create src/useOnline.d.ts with the following content:

declare module 'use-online' {
    export default function useOnline (): boolean
}

This will declare the module use-online which exports the function useOnline that returns boolean which is the online status.

Next, we will add a new script in package.json:

"scripts": {
    "prebuild": "npm i",
    "build": "babel src --out-dir dist",
    "postbuild": "copyfiles -u 1 ./src/useOnline.d.ts ./dist"
  },

The postbuild script will run after the build script is finished. It will copy src/useOnline.d.ts to the dist directory.

Last, we will add the types key in package.json:

"types": "dist/useOnline.d.ts",

This will make your package a Typescript package, although in Typescript packages you wouldn't really be doing it this way. This is just a simple form of how to do it.


Testing Our Custom Hook Locally

If you are adding your custom hook to your existing project, then you can probably just test it there. However, if you are creating a custom hook to publish online, and you want to test it as a separate package, this section is for you.

In the GitHub Repository I created for this tutorial,  you can see an example folder. This folder holds a website built using create-react-app that is just used to test our use-online package that holds the useOnline hook.

If you don't have a project to test use-online, let's create one just for the purpose by running the following command:

npx create-react-app example

This will create a new directory example that will hold a Single Page Application (SPA) built with React.

Before changing into that directory. Let's look into how we'd use use-online if it's not actually a package on NPM. As you probably already know, you can install any package on NPM using the install or i command like this:

npm install <PACKAGE_NAME>

However, how do we install a package that is only available locally? We will you linking.

npm-link allows us to create a symlink of our package in the global folder on our machine. This way, we can "install" local packages in other projects on our machine for purposes like testing.

What we will do is we will create a link of use-online, then use it in the example project we just created.

Inside the root directory of use-online run the following:

npm link

Once this is done, a symbolic link will be created to this package. We can now change to the example directory and "install" the use-online package by linking to it:

cd example
npm link use-online

Once linked, you can now use use-online in this project as if it was installed like any other NPM package. Any changes you make in use-online will automatically be portrayed in the package.

Before we can use use-online, let's go its root directory and run the build command:

npm run build

This will run NPM install, compiles the code with babel, then (if you followed along with the typescript part) copies the typescript declaration file to dist

I recommend before testing it you remove the node_modules directory. As we mentioned before, when using peerDependencies React will not be installed if the project you are installing use-online into already has it installed. However, when we ran the build command, the package was on its own and there was no react dependencies installed so it installed react. Since we are linking to it and not actually installing it in example, the node_modules directory of use-online will be inside the node_modules directory of example, which will lead to two react instances inside example. So, make sure to delete node_modules in use-online before testing it.

We will just be adding three 3 lines in example/src/App.js. First, we will import our custom hook:

import useOnline from 'use-online'

Second, inside the App component, we will use the useOnline hook to get the online state:

function App() {
  const online = useOnline()
  
  //... rest of the code
}

Third and last, we will add in the rendered part a condition to display to the user that they're offline:

return (
    <div className="App">
      <header className="App-header">
        {!online && <p>You're Offline</p>}
        <img src={logo} className="App-logo" alt="logo" />
        <p>
          Edit <code>src/App.js</code> and save to reload.
        </p>
        <a
          className="App-link"
          href="https://reactjs.org"
          target="_blank"
          rel="noopener noreferrer"
        >
          Learn React
        </a>
      </header>
    </div>
  );

Notice the line we added:

{!online && <p>You're Offline</p>}

When online is false, it means that the user is offline so we're showing them the message. Remember that the logic behind changing the state based on the user's connectivity is actually done inside useOnline. We just have to use the returned online value and everything else is done inside the custom hook.

Let's now start the development server by running:

npm start

It will just be the default React page that we see everytime we start a new create-react-app project:

The best way to test useOnline by simulating going offline. To do that, open the devtools then go to the Application tab

As you can see there's a checkbox to simulate an offline browser. This is used for testing service workers but it will still work for any kind of testing regarding the user's connectivity.

Once you check the Offline checkbox, you should see the "You're Offline" message we added:

Our custom hook works! Try turning it on and off. When you check the Offline checkbox, the message will show. When you check it off, the message will be removed.


Publishing Your Custom Hook

Now that we're done testing our custom hook, and we configured everything in our package, we are ready to publish it on NPM.

First, make sure you have an account on NPM. If you don't, you need to create one first.

In your terminal run:

npm login

You'll have to enter your username, password, and email. If it's all correct, you will be authenticated and authorized to publish your package.

In the root directory of your package, run:

npm publish

Unless any errors occur, that's all you'll have to do! Your package will be live once this command is done running.

If you get an error regarding an existing package with a similar name, make sure to rename your package inside package.json:

"name": "NEW_PACKAGE_NAME"

Then try again.

If your package was published successfully, you will receive an email to notify you about it and you can go ahead and view it on NPM. You can then inside your project run:

npm install PACKAGE_NAME

And it will be installed just like any package out there!

Updating Your Package

If you later on decided to fix some bugs or make any changes in your package and you want to update it, just run in the root directory of the package:

npm version TYPE

Where TYPE can either be patch (for small bug fixes), minor (for small changes), and major for big changes. You can read more about it here.