React Custom Hooks Tutorial - Creating useOnline, Testing and Publishing It

React Custom Hooks Tutorial - Creating useOnline, Testing and Publishing It

This was originally posted on my personal blog

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 isuseOnlinewhich 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 makes use of React's hooks like useState, useEffect, etc... You usually create custom hooks when a certain part of 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,useOnlinewill 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,useOnlinewill make use ofuseStatusto keep track of the user's connectivity, and will useuseEffectto register event listeners for the eventsonlineandofflineto set the state accordingly. In the end,useOnlinewill 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. Createsrc/useOnline.jswith the following content:

import { useState, useEffect } from 'react'

function useOnline () {

}

export default useOnline

We're just importinguseStateanduseEffectto use them in a bit, declaring the custom hookuseOnlineand 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);

}

onlinewill 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 ofnavigator.onLinewhich returns the online status of the browser.

Next, we need to listen to theonlineandofflineevents. Theonlineevent occurs when the user goes online, and theofflineevent occurs when the user goes offline. To add the listeners, we will useuseEffect:

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 insideuseEffectcallback. We are also passing an empty array as a second parameter foruseEffect. 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 ofonlinebased on the event. To do this, we will usesetOnline:

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 changes 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 inuseEffectthat removes the event listeners on unmount.

Since we will be usingremoveEventListenerto 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 outsideuseEffect(you can also add them inside instead) and we are passing them as the event listeners inaddEventListenerandremoveEventListenerinsideuseEffectfor both theonlineandofflineevents.

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 foruseOnlinewill 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 likeuseStateanduseEffectto 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 inpackage.json.

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

Package Information

When you first runnpm inityou 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 thepackage.jsonfile.

Note that thenameinpackage.jsonis 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 underdevDependencies.

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 ofreactis allowed. Meaning that your package shouldn't install React as well when installing it in a project.

So, let's changereactto 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 inpeerDependencies, thereactpackage 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 least16.8.0since 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 usingbabel:

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

Now, whenever we runbuild,prebuildwill run first to ensure that the dependencies required are installed, then the build script will compile the Javascript files in oursrcdirectory (which isuseOnline.js) and outputs the result indist.

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 themainfile in our package.

In our case, it will be the output of thebuildscript:

"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 theGitHub Repository,  you can see that there's anexampledirectory. 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 thefileskey:

"files": [
    "dist"
 ],

filestakes 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 thedistdirectory 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 createsrc/useOnline.d.tswith the following content:

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

This will declare the moduleuse-onlinewhich exports the functionuseOnlinethat returns boolean which is the online status.

Next, we will add a new script inpackage.json:

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

Thepostbuildscript will run after thebuildscript is finished. It will copysrc/useOnline.d.tsto thedistdirectory.

Last, we will add thetypeskey inpackage.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 theGitHub RepositoryI created for this tutorial,  you can see anexamplefolder. This folder holds a website built usingcreate-react-appthat is just used to test ouruse-onlinepackage that holds theuseOnlinehook.

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

npx create-react-app example

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

Before changing into that directory. Let's look into how we'd useuse-onlineif it's not actually a package on NPM. As you probably already know, you can install any package on NPM using theinstalloricommand like this:

npm install <PACKAGE_NAME>

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

npm-linkallows 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 ofuse-online, then use it in theexampleproject we just created.

Inside the root directory ofuse-onlinerun 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" theuse-onlinepackage by linking to it:

cd example
npm link use-online

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

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

npm run build

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

I recommend before testing it you remove thenode_modulesdirectory. As we mentioned before, when usingpeerDependenciesReact will not be installed if the project you are installinguse-onlineinto already has it installed. However, when we ran the build command, the package was on its own and there was noreactdependencies installed so it installedreact. Since we are linking to it and not actually installing it inexample, thenode_modulesdirectory ofuse-onlinewill be inside thenode_modulesdirectory ofexample, which will lead to tworeactinstances insideexample. So, make sure to deletenode_modulesinuse-onlinebefore testing it.

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

import useOnline from 'use-online'

Second, inside theAppcomponent, we will use theuseOnlinehook to get theonlinestate:

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>}

Whenonlineis 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 insideuseOnline. We just have to use the returnedonlinevalue 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 newcreate-react-appproject:

React Project]

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

DevTools

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:

Offline

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 onNPM. 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 insidepackage.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 bepatch(for small bug fixes),minor(for small changes), andmajorfor big changes. You can read more about it here.