Skip to main content

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();

useLocalStorage by @rehooks

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();

useArray by Aayush Jaiswal

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.

Counter.jsx
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.

useDocumentTitleHook.js
function useUpdateDocumentTitle(title) {
useEffect(() => {
document.title = title;
}, [title]);
}

Now we can use this new custom Hook in the Counter Component.

Counter.jsx
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.

Facebook example


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.