Custom Hooks
Learning Objectivesβ
- Create custom Hooks that can be reused in different apps
- Use state to keep track of data within the Hook
- Include useEffect's to manage the Hooks lifecycles
Framingβ
As with any project there comes the observation that the same snippets of code are being used in separate parts of the code base. This realization then drives the decision to structure that code into a reusable format so that it can be used whenever and wherever we need.
Let's jump into the way back machine to week 1 when we learned about Components and used them to recreate Bootstrap Cards
Recreating the Bootstrap Cards in React began with creating 2 separate Components with the same structural content but different values.
<Card1 title="Santorini" img="image" text="some text" url="some url"/>
<Card2 title="Zakynthos" img="image" text="some text" url="some url"/>
Seeing this pattern and knowing it wouldn't be practical to use it to create 100+ cards drove us to refactor into a single component that would allow us to create as many cards as were in the dataset.
const cards = cardsArr.map((ele, index) => {
return (
<Card
img={ele.img}
title={ele.title}
text={ele.text}
url={ele.url}
key={index}
/>
);
});
This reusability
aspect is something we all strive for when developing code.
Here is an additional example but specifically for jQuery.
β° Activity - 3minβ
In your dev career what functionality have you come across that you say the saw an use case to refactor into a reusable function and/or Class and then incorporate into other apps?
Take 2 min π to think about it and then post your answer in the thread.
So it looks we might already have a few use cases for custom Hooks.
Custom Hooks IIβ
In the world of React Custom Hooks
are meant to be flexible enough to be reused in many more apps then the one you are working on at this moment. In order to do this we must follow the rule of: predictable input = predictable output
.
What makes Custom Hooks unique is that they can leverage any of the existing Hooks, such as useState
, useEffect
, useRef
, ect along with any additional functionality needed.
Custom Hook Configurationsβ
As you begin working out the functionality of your custom hook you will also need to consider the following:
- which React hooks to include
- what needs to be exported
Some custom Hooks leverage only useState
, while others only useEffect
. Some some may require both or replace useState
with useReducer
.
Let's take a look at the following example that makes use of localStorage
and returns what appears to be a standard state
and corresponding setState
function.
useLocalStorageState by Gabe Ragland
const [count, setCount] = useLocalStorageState("countApp", 0);
setCount(count + 1);
Here is another Hook that also uses localStorage
but with some added functionality that allows the user to delete the localStorage
.
const [name, setName, deleteName] = useLocalStorage("name");
setName("localStorageName");
deleteName();
Here is a different example of a custom Hook that returns a single custom object with methods that seek to emulate the Array push
and splice
methods along with removing all the previous elements altogether.
const todos = useArray([]);
todos.add("learn hooks");
todos.remove("learn hooks");
todos.clear();
Our First Custom Hook - useDocumentTileβ
Let's take a look a really basic example of a Counter Component that also updates the document.title
of an open tab with text that display the current count
value.
Stater CodeSandbox: useDocumentTitle
It's not currently using any Custom Hooks but that will change once we evaluate what can be removed and placed into a Custom Hook.
function Counter() {
const [count, setCount] = useState(0);
const handleIncrement = () => setCount(count + 1);
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]);
return (
<>
<span>Current Count: {count}</span>
<section>
<button onClick={handleIncrement}>+</button>
</section>
</>
);
}
In this example it's clear that useEffect
is being called oncomponentDidUpdate
but only does so when the [count]
dependency value changes.
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]);
Letβs remove the useEffect
section out of the component and place it inside a new file called useDocumentTitleHook.js
.
Once the file is created let's create a new function which we will call useUpdateDocumentTitle
.
The function's only task is to update the document.title
if the title has changed.
function useUpdateDocumentTitle(title) {
useEffect(() => {
document.title = title;
}, [title]);
}
Now we can use this new custom Hook in the Counter Component.
function Counter() => {
const [count, setCount] = useState(0);
const handleIncrement = () => setCount(count + 1);
const title = `You clicked ${count} times`;
useUpdateDocumentTitle(title);
return (
<>
<span>Current Count: {count}</span>
<section>
<button onClick={handleIncrement}>+</button>
</section>
</>
);
};
This might seem like a useless Hook but the functionality it provides is used quite often. Take for instance how Facebook uses it to notify the user as to new or unread messages.
Solution CodeSandbox: useDocumentTitle
The Local Storage Hookβ
Let's take a look at the first local storage example and build it together. The functionality we are looking to achieve with this Hook is the following:
- create and set localStorage to some value
- return that value from localStorage
- update that value in localStorage
Which React Hooks To Useβ
Now it's time to consider which of the React Hooks it would need to perform this functionality.
Here is how it would be initialized and considering that it returns what looks like useState
values I'm sure it's safe to assume that it includes useState
.
const [count, setCount] = useLocalStorageState("countApp", 0);
setCount(count + 1);
Stater CodeSandbox: Counter useLocalStorage Hook Starter
So let's create the initial Hook and add state along with providing it the two params it's needs for the localStorage
name and the defaultValue
to set.
import React, { useState, useEffect } from "react";
export default function useLocalStorage(key, defaultValue) {
const [state, setState] = useState();
return [state, setState];
}
With our basic structure in place lets see if we can implement the first two features:
- create and set localStorage to some value
- return that value from localStorage
Here we will use a callback function when setting the initial state which will perform the following:
- retrieve the localStorage if it already exists or
import React, { useState, useEffect } from "react";
export default function useLocalStorage(key, defaultValue) {
const [state, setState] = useState(() => {
try {
const value = localStorage.getItem(key);
return value ? JSON.parse(value) : defaultValue;
} catch (e) {
console.log("e", e);
return defaultValue;
}
});
return [state, setState];
}
Now we need to do the following:
- update that value in localStorage when it changes
This sounds like a good use case for adding a useEffect
with a [dependency]
. When the dependency value changes this would then trigger useEffect
to run at which time we can set the value to localStorage
export default function useLocalStorage(key, defaultValue) {
const [state, setState] = useState(() => {
try {
const value = localStorage.getItem(key);
return value ? JSON.parse(value) : defaultValue;
} catch (e) {
console.log("e", e);
return defaultValue;
}
});
useEffect(() => {
localStorage.setItem(key, state);
}, [state]);
return [state, setState];
}
Solution CodeSandbox: Counter useLocalStorage Hook Solution
Custom Hooks Aboundβ
Since the release of the React Hooks, there has been an explosive growth of custom hooks. Thousands of React devs from all over the world have churned out custom hooks that simplify most of the arduous and boring tasks we do in React projects.
Here are some resources that provide custom hooks or walk you through the process of creating them yourself.