Posted on 10/5/2021
React hooks initially allow you to "hook into" React state and lifecycle features, like we used to do with the componentDidMount
or componentWillUnmount
methods when using class based components. What we'll discover in this series of articles is that we can implement our own custom hooks, using the few primitives hooks that React provides us (like useState
and useEffect
). This way, we can drastically reduce the cognitive complexity of our components, by moving away some logic into functions that we will be able to reuse anywhere in our React applications. The code will look cleaner, and we will be following the Single Responsibility Principle (SRP), which states that each class or function (or, in our case, component) should have responsibility over a single part of a program's functionality, and it should encapsulate that part.
Enough talk, let's get to work and implement our first custom hook: useBoolean
.Β π
First, why are we going to implement such a hook? Let's have a look at this simple component:
1const Spoil = ({ content }) => {
2 const [spoilVisible, setSpoilVisible] = useState(false);
3
4 return (
5 <div className="spoil">
6 <button onClick={() => setSpoilVisible(visible => !visible)}>
7 {spoilVisible ? "Hide" : "Show"}
8 </button>
9 {spoilVisible && <div className="spoil-content">{content}</div>}
10 </div>
11 );
12};
The component receives a content
prop, that only appears once the button gets clicked to show the spoil. Clicking the button again will hide it back, and so on.
Here, the component is so simple that it is already easy to read, but we could improve its readability by extracting the button onClick
listener to a separate function:
1const Spoil = ({ content }) => {
2 const [showSpoil, setShowSpoil] = useState(false);
3
4 const toggle = () => setShowSpoil((visible) => !visible)
5
6 return (
7 <div className="spoil">
8 <button onClick={toggle}>
9 {showSpoil ? "Hide" : "Show"}
10 </button>
11 {showSpoil && <div className="spoil-content">{content}</div>}
12 </div>
13 );
14};
That's better. We've simplified the return value of our function, but we've added a new line between the state initialization and the return statement. Of course this is not a major problem in our case, but when dealing with more complex components, this can lead to redundant functions declarations.
In fact, our component could be further simplified if we had a useBoolean
hook, that we would use like this:
1const Spoil = ({ content }) => {
2 const [showSpoil, setShowSpoil] = useBoolean(false);
3
4 return (
5 <div className="spoil">
6 <button onClick={setShowSpoil.toggle}>
7 {showSpoil ? "Hide" : "Show"}
8 </button>
9 {showSpoil && <div className="spoil-content">{content}</div>}
10 </div>
11 );
12};
See? We didn't add any extra function, and the onClick
listener is easier to read. Now, let's move into the implement of this simple hook, shall we? π
First, we define a function in which we can use the useState
hook.
1const useBoolean = (initialValue) => {
2 const [value, setValue] = useState(initialValue)
3
4 return [value, setValue]
5}
Be careful: we will only be able to use the useBoolean
function (or should I say hook) in React components, as it uses the useState
one.
So far, we've just created an alias for the useState
hook. Not very useful...π
The interesting part comes now: we will replace the setValue
function in the return array by an object that will contain 3 methods:
toggle()
to toggle the valueon()
to set the value to true
off()
to set the value to false
Our hook now looks like this:
1const useBoolean = (initialValue) => {
2 const [value, setValue] = useState(initialValue)
3
4 const updateValue = useRef({
5 toggle: () => setValue(oldValue => !oldValue),
6 on: () => setValue(true),
7 off: () => setValue(false)
8 })
9
10 return [value, updateValue.current]
11}
We've wrapped the object in the useRef
hook to prevent React from re-creating it at each re-render.
And here it is, you've just created your first custom hook, congratulations!Β π₯³
1const Articles = () => {
2 const [articles, setArticles] = useState([])
3 const [isLoading, setIsLoading] = useBoolean(false)
4 const [isError, setIsError] = useBoolean(false)
5
6 useEffect(() => {
7 setIsLoading.on()
8 fetch(someApiEndpoint)
9 .then(res => res.json())
10 .then(setArticles)
11 .catch(setIsError.on)
12 .finally(setIsLoading.off)
13 }, [])
14
15 return // ...
16}
You can't use setIsLoading(true)
as we don't export a function anymore but an object.
See how the above snippet is very easy to read?Β π
We've discovered how we can abstract some logic outside our components to make them lighter and easier to read. And that's
the main idea behind custom React hooks. In the next article, we will implement another useful one: useCounter
.