6.4 - Custom Hooks
React comes with several built-in Hooks like useState
, useContext
, and useEffect
. Sometimes, you’ll wish that there was a Hook for some more specific purpose: for example, to fetch data, to keep track of whether the user is online, or to connect to a chat room. You might not find these Hooks in React, but you can create your own Hooks for your application’s needs.
Let's say we want our app to show whether the user's network connection is on or off. We might want to include in our component:
- A piece of state that tracks whether the network is online.
- An Effect that subscribes to the global online and offline events, and updates that state.
We might start with something like this:
- App.js
import { useState, useEffect } from 'react';
export default function StatusBar() {
const [isOnline, setIsOnline] = useState(true);
useEffect(() => {
function handleOnline() {
setIsOnline(true);
}
function handleOffline() {
setIsOnline(false);
}
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
};
}, []);
return <h1>{isOnline ? '✅ Online' : '❌ Disconnected'}</h1>;
}
Now imagine you also want to use the same logic in a different component. You want to implement a Save button that will become disabled and show “Reconnecting…” instead of “Save” while the network is off.
To start, you can copy and paste the isOnline
state and the Effect into SaveButton
:
- App.js
import { useState, useEffect } from 'react';
export default function SaveButton() {
const [isOnline, setIsOnline] = useState(true);
useEffect(() => {
function handleOnline() {
setIsOnline(true);
}
function handleOffline() {
setIsOnline(false);
}
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
};
}, []);
function handleSaveClick() {
console.log('✅ Progress saved');
}
return (
<button disabled={!isOnline} onClick={handleSaveClick}>
{isOnline ? 'Save progress' : 'Reconnecting...'}
</button>
);
}
These two components work fine, but the duplication in logic between them is unfortunate. It seems like even though they have different visual appearance, you want to reuse the logic between them.
Extracting your own custom Hook from a component
Imagine for a moment that, similar to useState
and useEffect
, there was a built-in useOnlineStatus
Hook. Then both of these components could be simplified and you could remove the duplication between them:
function StatusBar() {
const isOnline = useOnlineStatus();
return <h1>{isOnline ? '✅ Online' : '❌ Disconnected'}</h1>;
}
function SaveButton() {
const isOnline = useOnlineStatus();
function handleSaveClick() {
console.log('✅ Progress saved');
}
return (
<button disabled={!isOnline} onClick={handleSaveClick}>
{isOnline ? 'Save progress' : 'Reconnecting...'}
</button>
);
}
Although there is no such built-in Hook, you can write it yourself. Declare a function called useOnlineStatus
and move all the duplicated code into it from the components you wrote earlier:
function useOnlineStatus() {
const [isOnline, setIsOnline] = useState(true);
useEffect(() => {
function handleOnline() {
setIsOnline(true);
}
function handleOffline() {
setIsOnline(false);
}
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
};
}, []);
return isOnline;
}
At the end of the function, return isOnline
. This lets your components read that value:
- App.js
- useOnlineStatus.js
import { useOnlineStatus } from './useOnlineStatus.js';
function StatusBar() {
const isOnline = useOnlineStatus();
return <h1>{isOnline ? '✅ Online' : '❌ Disconnected'}</h1>;
}
function SaveButton() {
const isOnline = useOnlineStatus();
function handleSaveClick() {
console.log('✅ Progress saved');
}
return (
<button disabled={!isOnline} onClick={handleSaveClick}>
{isOnline ? 'Save progress' : 'Reconnecting...'}
</button>
);
}
export default function App() {
return (
<>
<SaveButton />
<StatusBar />
</>
);
}
import { useState, useEffect } from 'react';
export function useOnlineStatus() {
const [isOnline, setIsOnline] = useState(true);
useEffect(() => {
function handleOnline() {
setIsOnline(true);
}
function handleOffline() {
setIsOnline(false);
}
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
};
}, []);
return isOnline;
}
Now your components don’t have as much repetitive logic. More importantly, the code inside them describes what they want to do (use the online status!) rather than how to do it (by subscribing to the browser events).
Naming Conventions
You must follow these naming conventions:
- React component names must start with a capital letter, like
StatusBar
andSaveButton
. React components also need to return something that React knows how to display, like a piece of JSX. - Hook names must start with
use
followed by a capital letter, likeuseState
(built-in) oruseOnlineStatus
(custom, like earlier on the page). Hooks may return arbitrary values.