REACT (https://www.kirupa.com/react/index.htm)
BOOK (https://www.amazon.com/exec/obidos/ASIN/0134546318/kirupacom)
Building Custom Hooks
by kirupa (https://www.kirupa.com/me/index.htm) | filed under Learning React (https://www.kirupa.com/react/index.htm)
By now, you've used a bunch of the hooks that come with React: useState, useEffect, useRef, useReducer, and useContext. What you may not have realized is that you can make your own. Custom hooks are one of the nicest ideas in React, and they are much simpler than the name makes them sound.
A custom hook is a way to take some logic that you wrote inside a component and move it into a function so that other components can use it too. In this tutorial, we'll see how to do that, and we'll build four hooks that you can use in your own apps: useToggle, useLocalStorage, useWindowSize, and a small useFetch.
Onwards!
Why Custom Hooks?
Let's say we have a panel that opens and closes when you click a button. It needs a piece of state to remember whether it's open, and a function to flip that state:
import { useState } from "react";
function Panel({ label }) {
const [open, setOpen] = useState(false);
function toggle() {
setOpen((current) => !current);
}
return (
);
}
This is fine. But toggling a boolean is something you do all the time: menus, modals, accordions, dark mode switches. If you copy and paste these few lines into every component that needs them, you end up with a lot of duplicates. If you later decide to change how toggling works, you have to find every copy.
When code is repeated like this, the usual solution in programming is to put it in a function. That's exactly what we are going to do, with one twist: this function is going to call hooks.
Your First Custom Hook
Here is the toggle logic pulled out into its own function:
import { useState } from "react";
function useToggle(initialValue = false) {
const [value, setValue] = useState(initialValue);
function toggle() {
setValue((current) => !current);
}
return [value, toggle];
}
That's a custom hook. There is nothing special about it beyond two things. It calls other hooks (here, useState), and its name starts with use. The function takes whatever arguments you like and returns whatever you like. In our case, it takes an optional starting value, and it returns an array with the current value and a function to flip it.
Our Panel gets a lot shorter now:
function Panel({ label }) {
const [open, toggleOpen] = useToggle(false);
return (
);
}
If you try this out, it behaves just as before. Click the button, and the label flips between open and closed. The component now reads as "I have something that toggles" instead of spelling out how toggling works.
Why the Name Starts with use
The use prefix is more than a style choice. React has a set of rules for hooks: they can only be called at the top level of a function component or another hook (never inside loops, conditions, or nested functions), so that React can match up each call with its stored state from one render to the next. Tools like the React ESLint plugin enforce these rules, and they decide whether a function is a hook by looking at its name. If your function calls hooks, name it useSomething and the tools will check it for you. If it doesn't call any hooks, it can be a plain function and doesn't need the prefix at all.
Sharing Logic, Not State
This is the part that surprises people the most. Let's use two panels:
function Panels() {
return (
<>
>
);
}
If you click the first button, only the first panel changes. The second one stays closed. Each time a component calls useToggle, it gets its own, completely separate copy of the state. Here is a picture of what that looks like:
[Image: A useToggle box at the top with arrows pointing to Panel One and Panel Two. Panel One has its own state, open: true, and Panel Two has its own state, open: false.] (https://www.kirupa.com/react/images/custom_hook_state_144.png)
A custom hook shares the logic, but not the state. The code inside the hook is written once. Each component that calls it gets its own state, just like calling useState directly inside the component would. It's as if you had pasted the hook's body into each component.
This is an important distinction. If you do want several components to share the same state, a custom hook alone won't do it. You would keep the state in a common parent and pass it down, or put it in a context like we saw in Sharing Data with Context (https://www.kirupa.com/react/sharing_data_with_context.htm).
useLocalStorage
Our next hook is more useful. Browsers have a place called localStorage where you can save small bits of text that stick around even if the person closes the tab. A hook that works like useState but saves its value there is handy for things like preferences or a draft of something someone is writing.
import { useEffect, useState } from "react";
function useLocalStorage(key, initialValue) {
const [value, setValue] = useState(() => {
try {
const saved = localStorage.getItem(key);
return saved !== null ? JSON.parse(saved) : initialValue;
} catch {
return initialValue;
}
});
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue];
}
There is a bit going on here, so let's go through it. We pass a function to useState instead of a value. This is called a lazy initializer, and React only calls it the first time the component renders. That's helpful because reading from localStorage is not free, and we don't want to do it on every render. Inside, we look for a saved value under our key. If we find one, we turn the text back into a value with JSON.parse. If we don't (or if something goes wrong, like storage being disabled), we use the initial value.
The second half is an effect. Whenever the value (or the key) changes, we save it. Because it returns the same [value, setValue] pair that useState does, the hook is a drop-in replacement. Here is a notes box that remembers what you typed:
function Notes() {
const [note, setNote] = useLocalStorage("note", "");
return (