Skip to main content

Writing custom hooks in React

· 4 min read
Luke Owen
Lead Front End Developer @ Lunio

Hooks came into play in React 16.8, they allow you to manage state and side effects in functional components.

The default React hooks are pretty useful, but sometimes you’ll find yourself repeating the same logic, we can extract this into a custom hook and reuse it.

Custom hooks

Traditionally if we wanted to share functionality between components we'd use a pattern like render props or higher order components.

With hooks, we can extract some logic into a function and reuse it in multiple components.

They're really useful when you need to share code across projects, and they're common in libraries, for example react-router has a useHistory hook that returns the history object.

Common use cases

Custom hooks are commonly used for:

Managing state

You can combine a custom hook with the Context API to create a global state management system

Wrapping external libraries

You're most likely using a bunch of external libraries, you can wrap them in a custom hook and consume them in your components.

This keeps the code in one place, and it's really easy to swap the library for another if you need to.

Fetching data

You can use a custom hook to fetch data from an API, and then use the data in your component.

Sharing logic between projects

If you have several projects or apps that use similar logic, you can extract it into a custom hook and reuse it.

Writing a custom hook

Custom hooks are just functions that start with use, and they can call other hooks.

Here's a really simple example, we'll create a hook that increments a counter.

We've got a function that uses the useState hook to create a counter, and then returns an object with the current count, an increment function and a reset function.

import { useState } from 'react';

function useCounter() {
const [count, setCount] = useState(0);

const increment = () => setCount(count + 1);
const reset = () => setCount(0);

return {
count,
increment,
reset,
};
}

export default useCounter;

We can then use this hook in our component.

We can now use this hook throughout our application, and maintain the logic in one place.

import useCounter from './useCounter';

function Counter() {
const { count, increment, reset } = useCounter();

return (
<div>
<span>{count}</span>
<button onClick={increment}>Increment</button>
<button onClick={reset}>Reset</button>
</div>
);
}

Passing arguments

We can pass arguments to a custom hook as with any other function.

In this example we'll pass an initial count to the hook.

function useCounter(initialCount = 0) {
const [count, setCount] = useState(initialCount);

const increment = () => setCount(count + 1);
const reset = () => setCount(initialCount);

return {
count,
increment,
reset,
};
}

We can then pass an initial value when we use it.

function Counter() {
const { count, increment, reset } = useCounter(10);

return (
<div>
<span>{count}</span>
<button onClick={increment}>Increment</button>
<button onClick={reset}>Reset</button>
</div>
);
}

Testing hooks

Hooks are pretty easy to test, as you can just call them like any other function.

To test this example I'll react-testing-library, specifically the react-hooks extension.

The renderHook function will render the hook without having to render a component. It returns an object with the result of the hook.

import { renderHook, act } from '@testing-library/react-hooks';
import useCounter from './useCounter';


describe('useCounter', () => {
it('should return the initial count', () => {
const { result } = renderHook(() => useCounter());

expect(result.current.count).toBe(0);
});

it('should increment counter', () => {
const { result } = renderHook(() => useCounter());

act(() => {
result.current.increment();
});

expect(result.current.count).toBe(1);
});
});

Rules of hooks

The React rules of hooks still apply to custom hooks, so it's worth refreshing yourself on them.

Only call hooks at the top level

You can't call hooks conditionally, or within loops.

Hooks need to be called in the same order on every render, so you can't call them in anything conditional.

Only call hooks from React functions

Hooks need to be called from a React functional component or a custom hook.

They need to live in the context of React, so you can't call them from regular JavaScript functions.