Building Custom React Hooks
React’s official documentation doesn’t cover custom hooks in detail as it covers the basic hooks, so the focus of this article is going to be primarily on building custom React hooks and best practices.
React hooks simplify the process of creating reusable, clean and versatile code, and advanced optimization techniques like memoization are now more accessible and easier to use. React’s official documentation doesn’t cover custom hooks in detail as it covers the basic hooks, so the focus of this article is going to be primarily on building custom React hooks and best practices.
Understanding basic React hooks are required to get the most out of this article. If you aren’t already familiar with the basics, there are numerous great articles out there covering them. For example, React’s official docs are a great place to start.
Mindset
In order to build a versatile, performant and reusable custom hook, there are several things to keep in mind.
Hooks run each time component re-renders
Since we’re working with functional components and hooks, we no longer have the need for lifecycle methods. Each time state or a prop is changed, the functional component is being re-rendered and, thusly, our custom hook is being called over and over again.
Use basic hooks as much as possible
Basic React hooks are the core of any custom hook. We can use memoization and hook dependency arrays to control which parts of our custom hook will change or will not change with each re-render. It’s important to understand the role each basic hook can have in our custom hook in order to use them effectively and build performant hooks.
Rules of hooks
There are a few important rules to keep in mind. These rules are explained in detail on React hooks documentation.
Building the custom hook
Now that we have covered the basics, we’re ready to build our own custom hook. In the following example, we’ll establish a solid pattern for building custom hooks and go through some of the best practices.
Let’s imagine that we’re working on a project where users can play multiple games that use dice rolls as part of their game mechanic. Some games require only a single dice to play and some games may require multiple dice to play. We’ll also assume that during some games, the number of dice used may change.
Keeping that in mind, we are going to build useGameDice hook with the following features:
- The custom hook can be initialized with a number of dice being used and an initial value
- A function that sets the number of dice being used
- A function that rolls the dice. Returns an array of random numbers between 1 and 6. Length is determined by the number of dice being used
- A function that resets all dice values to initial value
Setting up the hook (imports and hook function)
We are declaring our custom hook as a regular arrow function using the recommended convention of naming custom hooks — the name should start with the “use” keyword. We are also importing React hooks that we’ll use later on in our implementation. We could also import constants, other functions, other custom hooks, etc.
Our hook can be initialized with 2 optional variables:
- initialNumberOfDice — how many dice will be used
- initialDiceValue — determines the initial value and value after reset
Both variables have a default value of 1 to avoid any errors and simplify the hook setup.
import { useState, useMemo, useCallback, useEffect } from "react";
export const useGameDice = (initialNumberOfDice = 1, initialDiceValue = 1) => {
/* We’ll be adding code here in order */
};
Adding state and memoized private variables
First, we need to set up our state. We’ll declare two simple states:
- diceValue — array which size is defined by numberOfDice and holds value for each dice
- numberOfDice — determines the number of dice (diceValue array size) that will be used
We are also initializing initialDiceState variable that creates the initial array value that will be assigned on initial render and state reset. This value is memoized to avoid array being initialized and filled with default values on each re-render.
const [diceValue, setDiceValue] = useState();
const [numberOfDice, setNumberOfDice] = useState(initialNumberOfDice);
const initalDiceState = useMemo(
() => Array(numberOfDice).fill(initialDiceValue),
[numberOfDice, initialDiceValue]
);
Adding memoized hook functions
Next, we’ll create the following functions:
- generateRandomDiceNumber — generates a random number between 1 and 6 (a single dice roll)
- rollDice — calls a random number generator for each element in the array (dice)
- resetDice — resets the dice value state to an initial value
const generateRandomDiceNumber = useCallback(() => {
return Math.floor(Math.random() * 6) + 1;
}, []);
const rollDice = useCallback(() => {
const arrayConfig = { length: numberOfDice };
const newDiceValues = Array.from(arrayConfig, generateRandomDiceNumber);
setDiceValue(newDiceValues);
}, [numberOfDice, generateRandomDiceNumber]);
const resetDice = useCallback(() => {
setDiceValue(initalDiceState);
}, [initalDiceState]);
We are using useCallback hook to control when the functions are going to be re-initialized. Functions are re-initialized only when any variable in their dependency array changes. In the case of the generateRandomDiceNumber function, it’s never re-initialized after the first render and initialization because this function doesn’t depend on any external variable or state.
Adding side effects — hook initialization & update
We need to set up a listener that will watch for updates to our initial dice state. This side effect has two responsibilities:
- It sets the dice state to the initial value when the hook is first initialized
- It updates the dice state to the initial value when dice number (array size) has changed
useEffect(() => {
setDiceValue(initalDiceState);
}, [initalDiceState]);
API setup & return statement
Finally, we are defining our state and api objects and returning them in an array, following the useState convention. Let’s take a look at each object:
- state — holds all our state values. We expect this object to change on almost every re-render
- api — holds all functions. We are returning some of our functions declared in useCallback and a function from useState hook. This object is memoized because we do not expect this to change on almost every re-render
const state = {
diceValue,
numberOfDice
};
const api = useMemo(
() => ({
setNumberOfDice,
rollDice,
resetDice
}),
[setNumberOfDice, rollDice, resetDice]
);
return [state, api];
We are returning the objects in an array because we want this hook to be flexible. By doing so, we allow developers to rename the returned variables and allow them to initialize multiple instances of this hook if needed.
const [diceFirst_state, diceFirst_api] = useGameDice();
const [diceSecond_state, diceSecond_api] = useGameDice();
Git repository & demo
You can see the final implementation and full code with a demo on the following GitHub repository.
React custom hooks pattern overview
By now, you might have noticed that we grouped the code we were adding in sections. This structured and clean pattern follows a logical path:
- State initialization (useState, useReducer), local variables initialization (useMemo), ref initialization (useRef) & external custom hooks initialization
- Memoized functions (useCallback)
- Side effects (useEffect)
- API setup (state and memoized API)
- Return statement
Conclusion
It’s no surprise that hooks were well received by React community. Developers are able to share logic between components more easily, create multiple components (interfaces) for each custom hook, pick and choose the parts of hook’s state and API they’ll use in their components, etc.
This reusability and versatility make hooks a real game-changer in React app development. With an established pattern and best practices when building custom React hooks, developers are able to deliver code with consistent quality, clear structure, and optimal performance.
Thank you for taking the time to read the article. If you’ve found it helpful, please give this article a clap and share it.