Creating custom hooks in ReactJS

Creating custom hooks in ReactJS

Introduction

Do these scenarios sound familiar to you? You need a loading state, a success and error state from the fetch API, a mechanism to cache data, to check the current mouse pointer, and on and on, all in different components.

This logic gets repeated multiple times across multiple components. If you take a closer look, you will find that this scenario requires you to have its own state, which depends on the life cycle method such as useEffect. You can go ahead and write the logic for this in each of your components, or you can create a common wrapper that has its own state variables and useEffect that helps to maintain its own isolated state.

This wrapper is called a hook in ReactJS. In this article, we are going to unravel all of the mysteries related to custom hooks and take a look at a good number of examples of creating our own custom hooks.

Without further ado, let’s get started!

Prerequisites

It is recommended to go through the below articles to get the most out of this one:

What are hooks?

A hook is a concept that was introduced in React v16.8. Hooks are wrappers that help to encapsulate repeated stateful logic so that it can be used and shared later across multiple components.

It is a feature that allows you to use all class-based components’ life cycle methods in functional-based components.

It lets you use useState and other React features without a class.

Having said that, React allows you to create your own custom hooks. A custom hook is a way to share stateful logic across components. We can follow HOC and render props patterns to share this stateful logic across components; custom hooks are just another way to do it.

How to create a custom hook

To create your own custom hook, React tells us to follow certain guidelines. These guidelines are written on the page rules of hooks. Here is a summary of these rules:

  • Your custom hook should start with the keyword use, for example, useAuthState, useFetch, etc.
  • Hooks should be called at the top of the component. We should avoid using hooks inside a condition or a loop because React will be uncertain about the order of execution of the hooks. React knows about each hook and its associated value via the order of execution of hooks during rendering.It is expected that at each render, the order of hooks should remain the same. In this way, React preserves the state.
  • Hooks should be called from a function-based component.
  • You can call one hook from another hook.
  • Every call to the hook gets an isolated state.

Now that we know some basic rules of hooks, let’s create one simple example:

import { useEffect, useState } from "react";

export const useAuthState = () => {

const [accessToken, setAccesToken] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
const [isError, setIsError] = useState<boolean>(false);

useEffect(() => {
const localStorageAccessToken = window.localStorage.getItem("accessToken");
if (localStorageAccessToken) {
setAccesToken(localStorageAccessToken);
} else {
fetch("random. justyy.workers.dev/api/random/?cached&n=128")
.then((item) => item.json())
.then((data) => {
setAccesToken(data);
setIsError(false);
})
.catch((err) => {
setIsError(true);
setError(err);
});
}
}, []);
return {accessToken, isError, error}
};

Save to Pieces

This is a pretty basic example of a custom hook. In this hook, we are doing the following things:

  • We are returning an access token, isError, and error state.
  • The purpose of this hook is to return the access token from the local storage or fetch a new one if the access token is not present in the local storage.
  • We have named this hook useAuthState. This satisfies the fact/rule of hook that the name of the hook should start with use.
  • If you carefully observe the code inside this hook, you will find that we need this each and every time we need to use the access token.
  • This can create a lot of redundant code pretty quickly.
  • To avoid this, we can make use of such hooks and use this logic in whichever component we require.
  • While the hook is used, it gets its own isolated state.

The usage of this component is also simple. Just import the hook as follows:

import "./styles.css";
import { useAuthState } from "./CustomHooks";

export default function App() {
const {accessToken, isError, error} = useAuthState();
return (
<div className="App">
<span>{accessToken}</span>
<br/>
<span>isError = {JSON.stringify(isError)}</span>
</div>
);
}

Save to Pieces

Now that we have a basic understanding of custom hooks, let’s dive deep into some more examples of custom hooks.

Creating a custom hook to fetch data

There might be scenarios where you want to fetch data from an API, but you also need the following data:

  • You need to know if data is being fetched, i.e., isLoading
  • You need to know if the data has been fetched successfully
  • You need to know if the data was not fetched and has led to an error

In all of the above scenarios, you would do something like this:

const [data, setData] = useState<unknown | null>(null);
const [error, setError] = useState<string>("");
const [isError, setIsError] = useState<boolean>(false);
const [isSuccess, setIsSuccess] = useState<boolean>(false);
const [isLoading, setIsLoading] = useState<boolean>(false);


useEffect(() => {
(async () => {
try {
setIsLoading(true);
const resp = await fetch(url);
const respData = await resp.json();
setData(respData);
setIsLoading(false);
setIsSuccess(true);
} catch (err) {
setIsLoading(false);
setIsSuccess(false);
setIsError(true);
setError(err as string);
}
})();
}, [url]);

Save to Pieces

You would make use of multiple useStates and one useEffect to get the data on page load. Imagine you are repeating this multiple times across multiple components.

In such cases, it is suitable for us to create a hook that will abstract this stateful logic. Then we can simply get these values/states as a return value of the hook.

Here we go: we’re going to call useFetch as a hook that will fetch the data for us from the given URL/API.

export const useFetch = (url: string) => {
const [data, setData] = useState < unknown | null > (null);
const [error, setError] = useState < string > ("");
const [isError, setIsError] = useState < boolean > (false);
const [isSuccess, setIsSuccess] = useState < boolean > (false);
const [isLoading, setIsLoading] = useState < boolean > (false);

useEffect(() => {
try {
setIsLoading(true);
setTimeout(async () => {
const resp = await fetch(url);
const respData = await resp.json();
setData(respData);
setIsLoading(false);
setIsSuccess(true);
}, 2000);
} catch (err) {
setIsLoading(false);
setIsSuccess(false);
setIsError(true);
setError(err as string);
}
}, [url]);
return {
data,
error,
isError,
isSuccess,
isLoading
};
};

Save to Pieces

We just wrapped the above code inside a custom hook. There are a couple of things to note about our custom hook:

  • It has multiple useStates such as isLoading, isSuccess, etc., that tell us the state of the API at different phases
  • isLoading - This is set to true at the start whenever we start fetching the data inside the useEffect. Once the data is fetched or an error is thrown, we set its value to false. We are doing this because we need a way to know that the hook is doing its work. While the hook is working, we can show a spinner on the UI with the help of this toggle. Its default value is false.
  • data and error - These two states, as the names suggest, store the data and the error that is thrown by the fetch API.
  • isError - This is a toggle state used to show whether an error has occurred or not. Its default value is false, but if we get an error during fetch API, then we set its value to true.
  • isSuccess - This is a toggle state that is set to true whenever the data from the fetch API is fetched successfully. Its default value is false.
  • One thing to note in the above hook is we have introduced a setTimeout API inside the useEffect. This is done because we want to display the loading state. Since the data is fetched quickly, to showcase the loading state we introduced the setTimeout API.

The code below demonstrate how we consume this hook:

import "./styles.css";
import { useFetch } from "./CustomHooks";

export default function App() {
// const { accessToken, isError, error } = useAuthState();
const {isLoading, data, isSuccess} = useFetch(
"jsonplaceholder.typicode.com/todos/1"
);

return (
<div className="App">
<hl>Custom hook example</hl>

{isLoading && <span>Loading </span>}
{isSuccess && <span>data - {JSON.stringify(data, null, 2)}</span>}
</div>

);
}

Save to Pieces

A custom hook to track mouse position

There might be certain scenarios where you might want to track your mouse movement from the edges of the browser inside a specific div or you want to track it over the entire page. In any case, a custom hook that can return the x, and y coordinates along with a handler function that will manage the track of the mouse event inside an element will do a fine job.

So here we go, we are going to name this hook as useMousePosition. Below is the code for the same:

export const useMousePosition = (global:boolean= true) => {
const [mouseCoords, setMouseCoords] = useState < {
x: number;
y: number;
} > ({
x: 0,
y: 0,
});

const handleMouseMovement = (event: MouseEvent): void => {
setMouseCoords({
X: event.screenX,
y: event.screenY,
});
};

useEffect(() => {
if (global) {
window.addEventListener("mousemove", handleMouseMovement);

return () => {
window.removeEventListener("mousemove", handleMouseMovement);
};
}
}, [global]);

return [mouseCoords, handleMouseMovement];
};

Save to Pieces

Here is a bit of explanation for the above code:

  • First, it has a useState as mouseCoords that has default values of x and y to be 0. This state is used to store the x and y coordinates of the mouse.
  • Next, we have a handleMouseMovement function. This function is used as an event handler on mousemove events.
  • Then, we have a useEffect that adds an event listener on page load for the event mousemove. This event listener is removed when the component is unmounted from the DOM. We do this operation when we want to track the mouse movement across the entire page. This is why we attached the event listener to the window object.To track the mouse position on the entire page, we use a toggle variable called global. If it's true, then we track the entire mouse movement.
  • If global is set to false, then the code inside the useEffect is not used and the mouse position tracker completely relies on handleMouseMovement.
  • In such a case, while consuming this hook we can pass on the handleMouseMovement function to the target element’s onMouseMove property to track the cursor position inside that element.

Here is how you will be consuming this hook while targeting mouse tracking on a specific div element:

import "./styles.css";

import { useMousePosition } from "./CustomHooks";

export default function App() {
const [mouseCoords, handleMouseMove] = useMousePosition(false);
return (
<div className="App">
<hl>Custom hook example</hl>
<div
onMouseMove={handleMouseMove as any}
style={{
width: "400px",
height: "400px",
backgroundColor: "#cacaca",
display: "flex",
alignItems: "center",
justifyContent: "center"
}}
>
<strong>useMousePosition</strong>
<span>{JSON.stringify(mouseCoords, null, 2)}</span>
</div>
</div>

);
}

Save to Pieces

A custom hook to get CSS variables

CSS variables are entities that can be created and reused later inside a document. They are also called custom properties. They are denoted with a special notation like --primary-color and are accessed using a special function called var().

In large projects, there can be scenarios where styles, colors, or sizes get repeated at multiple locations. This can get quite difficult to manage and it can also become infeasible to remember these common values.

So, it’s always better to have a central place where you can store these values and use them later. You can consider this a central repository of style properties. These variables, once created, can be accessed anywhere in the document via CSS‘s var() function only when you declare these variables inside :root pseudo-classs selector.

If we compare this methodology in React, then the CSS variables inside these :root classes would act as a global state.

Now, enough of this intro! Let’s get started with creating actual CSS variables. Let’s create two variables --primary-color and --font-size-large.

Since we’re using a CRA app, by default there’s a styles.css file inside the src folder. If not, create the file and paste the following code in it:

:root {
--primary-color: #cacaca;
--font-size--large: 32px;
}

Save to Pieces

A simple usage of these variables would be similar to below:

:root{
--primary-color: #561d25;
--font-size-large: 16px;
}

span {
color: var(--primary-color);
font-size: var(--font-size-large);
}

Save to Pieces

Let’s do some of the fun part here! Consider a scenario where you need to access these CSS variables in your component and change it based on a certain value of an API.

You will need to do following:

  • Create a state variable inside a component
  • Create a useEffect and use JavaScript’s getComputedStyle function to access the styles of the :root element
  • Use the setProperty function to set the new or existing CSS variable values
  • Repeat this logic across multiple components

Repeating this logic everywhere would be a hassle. To manage this better, let’s create a custom hook named useCSSVariable to do exactly the same thing.

Copy-paste the below code in your utility or CustomHooks.ts file (if one doesn’t exist, then you can create this file):

export const useCSSVariable = (targetElement:string = ":root") => {
const [
cssRootStyles,
setCssRootStyles
] = useState<CSSStyleDeclaration | null>(null);
const [rootElement, setRootElement] = useState<Element | null>(null);

const getCssVariable = (variable: string) => {
const value = cssRootStyles?.getPropertyValue(variable);
return value;
};

const setCssVariable = (variable: string, value: string) => {
//@ts-ignore
rootElement?.style.setProperty(variable, value);
};
useEffect(() => {
if (document) {
const rootElement = document.querySelector(targetElement) as Element;
const rootStyles = getComputedStyle(rootElement);
setRootElement(rootElement);
setCssRootStyles(rootStyles);
}
}, [targetElement])

return {getCssVariable, setCssVariable};
};

Save to Pieces

Ok, there are some new moving parts to this hook, but trust me. The pattern of creating a custom hook remains the same:

  • useCSSVariable accepts targetElement as an argument. targetElement is the CSS selector for an element in which the CSS variables are stored.
  • Next, we create two state variables: cssRootStyles and rootElement.
  • cssRootStyles is a state variable that stores all the computed styles of the targetElement via getComputedStyle.
  • rootElement is another state variable that will store the targetElement. We will use this state to set the CSS variable using setProperty function.
  • Then, we have a useEffect that sets the rootElement and cssRootStyles as a targetElement and computedStyles of the root element.
  • We also have getCssVariable, which is a simple getter function that gets the CSS variable’s value with the help of getPropertyValue. We return the same value from this function.
  • We also have setCssVariable, which takes the CSS variable’s name and value and stores them with the help of the setProperty function.
  • Finally, we return these two functions so that once the consumer component is mounted, we can start using it directly.

Here is the usage of the useCSSVariable hook:

import "./styles.css";
import { useCSSVariable } from "./CustomHooks";

export default function App() {
const {getCssVariable, setCssVariable} = useCSSVariable();
const handleOnClick = () => {
setCssVariable("--secondary-color", "#CE8147");
};
return (
<div className="App">
<hl>Custom hook examples</hl>
<br/>
<br/>
<div className="css-variables">
<strong>useCSSVariable - </strong>
<br/>
<span>Primary Color - </span>
<span>{getCssVariable("--primary-color")}</span>
<br/>
<span>Font Size Large - </span>
<span>{getCssVariable("--font-size-large")}</span>
<br/>
<span> Seconadry Color - </span>
<span>{getCssVariable("--secondary-color")}</span>
<br/>
<button onClick={handleOnClick}>Create New CSS Variable</button>
</div>
</div>
);

}

Save to Pieces

Here in App.js we simply call the useCSSVariable hook. We finally make use of the getCssVariable function to fetch the --primary-color and --font-size-large CSS variables.

Finally, I created a simple button that creates a new CSS variable --secondary-color with the value #CE8147 on click of the of it.

Summary

So, we learned about hooks and custom hooks in React. We also learned about how we can create our own custom hooks along with some interesting examples, such as a CSS variables fetcher, a fetch API hook, etc.

If you are looking to wrap your stateful logic and share it across multiple components, then you should definitely give custom hooks a try.

Thanks for reading!

Become More Productive Writing Your React Apps

Every component you create in React often requires tons of repetition no matter what you do, and across every frontend project, there are always custom setups and best practices that teams follow. Pieces helps you solve this for any React project by allowing you to create a local micro-repository where you can store any code snippets along with relevant metadata straight on your machine. Additionally, Pieces makes it incredibly easy to share your snippets with others, form collections to onboard others onto a project, and even has integrations to allow you to use your snippets directly in your IDE. Our team of developers is making changes every day to make the most effective and efficient micro-repo for you.

Try Pieces today!