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.