React Custom Hooks Tutorial - Creating useOnline, Testing and Publishing It
Tags: webdev, react, javascript, tutorial
Published: 2021-04-08 10:02:58

_This was originally posted on [my personal blog](https://blog.shahednasser.com/react-custom-hooks-tutorial-creating-useonline/)_

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](https://github.com/shahednasser/use-online).

---

### 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:

```bash
mkdir use-online
cd use-online
```

Then, we'll initialize our NPM project:

```bash
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:

```bash
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:

```js
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:

```js
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`:

```js
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`:

```js
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 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:

```js
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:

```js
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:

```json
"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:

```json
"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`:

```json
"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:

```js
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:

```json
"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](https://github.com/shahednasser/use-online), &nbsp;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:

```json
"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:

```ts
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`:

```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`:

```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](https://github.com/shahednasser/use-online)I created for this tutorial, &nbsp;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:

```bash
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:

```bash
npm install <PACKAGE_NAME>
```

However, how do we install a package that is only available locally? We will you[linking](https://docs.npmjs.com/cli/v7/commands/npm-link).

`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:

```bash
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:

```bash
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:

```bash
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:

```js
import useOnline from 'use-online'
```

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

```js
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:

```js
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:

```js
{!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:

```js
npm start
```

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

![React Project](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/l07s1f17d65tcpqxjg12.png)

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

![DevTools](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1klmh56dk314jn3u7dmr.png)

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dluxm6olhqi4pgr7kfgb.png)

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](https://www.npmjs.com/). If you don't, you need to create one first.

In your terminal run:

```bash
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:

```bash
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`:

```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:

```bash
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:

```bash
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](https://docs.npmjs.com/about-semantic-versioning).