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 isuseOnline
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 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,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 ofuseStatus
to keep track of the user's connectivity, and will useuseEffect
to register event listeners for the eventsonline
andoffline
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. Createsrc/useOnline.js
with the following content:
import { useState, useEffect } from 'react'
function useOnline () {
}
export default useOnline
We're just importinguseState
anduseEffect
to use them in a bit, declaring the custom hookuseOnline
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 ofnavigator.onLine
which returns the online status of the browser.
Next, we need to listen to theonline
andoffline
events. Theonline
event occurs when the user goes online, and theoffline
event 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 insideuseEffect
callback. 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 ofonline
based 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 inuseEffect
that removes the event listeners on unmount.
Since we will be usingremoveEventListener
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 outsideuseEffect
(you can also add them inside instead) and we are passing them as the event listeners inaddEventListener
andremoveEventListener
insideuseEffect
for both theonline
andoffline
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 foruseOnline
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 likeuseState
anduseEffect
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 inpackage.json
.
In the beginning, we installed@babel/cli
andcopyfiles
. This is where we'll put them into use.
Package Information
When you first runnpm 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 thepackage.json
file.
Note that thename
inpackage.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 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 ofreact
is allowed. Meaning that your package shouldn't install React as well when installing it in a project.
So, let's changereact
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 inpeerDependencies
, thereact
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 least16.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 usingbabel
:
"scripts": {
"prebuild": "npm i",
"build": "babel src --out-dir dist"
},
Now, whenever we runbuild
,prebuild
will run first to ensure that the dependencies required are installed, then the build script will compile the Javascript files in oursrc
directory (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 themain
file in our package.
In our case, it will be the output of thebuild
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 theGitHub Repository, you can see that there's anexample
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 thefiles
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 thedist
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 createsrc/useOnline.d.ts
with the following content:
declare module 'use-online' {
export default function useOnline (): boolean
}
This will declare the moduleuse-online
which exports the functionuseOnline
that 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"
},
Thepostbuild
script will run after thebuild
script is finished. It will copysrc/useOnline.d.ts
to thedist
directory.
Last, we will add thetypes
key 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 anexample
folder. This folder holds a website built usingcreate-react-app
that is just used to test ouruse-online
package that holds theuseOnline
hook.
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 directoryexample
that will hold a Single Page Application (SPA) built with React.
Before changing into that directory. Let's look into how we'd useuse-online
if it's not actually a package on NPM. As you probably already know, you can install any package on NPM using theinstall
ori
command like this:
npm install <PACKAGE_NAME>
However, how do we install a package that is only available locally? We will youlinking.
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 ofuse-online
, then use it in theexample
project we just created.
Inside the root directory ofuse-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" theuse-online
package by linking to it:
cd example
npm link use-online
Once linked, you can now useuse-online
in this project as if it was installed like any other NPM package. Any changes you make inuse-online
will 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_modules
directory. As we mentioned before, when usingpeerDependencies
React will not be installed if the project you are installinguse-online
into already has it installed. However, when we ran the build command, the package was on its own and there was noreact
dependencies installed so it installedreact
. Since we are linking to it and not actually installing it inexample
, thenode_modules
directory ofuse-online
will be inside thenode_modules
directory ofexample
, which will lead to tworeact
instances insideexample
. So, make sure to deletenode_modules
inuse-online
before 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 theApp
component, we will use theuseOnline
hook to get theonline
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>}
Whenonline
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 insideuseOnline
. We just have to use the returnedonline
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 newcreate-react-app
project:
The best way to testuseOnline
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 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), andmajor
for big changes. You can read more about it here.