/

Engineering

Feb 15, 2025

Feb 15, 2025

Creating React custom hooks with Pieces

Discover what custom React hooks are, how to create one, and why they are essential for writing reusable components.

A large hook on a platform next to a body of water.
A large hook on a platform next to a body of water.
A large hook on a platform next to a body of water.

My React codebase used to be cluttered with repetitive state management logic. I often found myself duplicating the same lines of code across multiple components. That changed when I discovered React custom hooks.

They’re incredible for encapsulating common logic, like data fetching in a clean and maintainable way. Understanding why we use custom hooks in React is key. That’s why in this article, I'll share what I've learned about React hooks and show you how to create your own custom hooks.


Hooks at a glance

React Hooks are reusable functions that let you manage state and lifecycle events in functional components. Think of them as special JavaScript functions with built-in reactivity. They were first introduced in February 2019 as a modern solution to class components and Higher-Order Components (HOCs).

React provides several built-in hooks like useState, useReducer, and useEffect, but it is also possible to create custom hooks to solve your particular use case.


How to create your first custom hook

When I created my first custom hooks in React, I learned the hard way that there are some critical rules to follow. Here’s what I discovered:

  1. Hook Namimg: Hook names have to start with `use` (like useCounter)

  2. Top-level Calls: Hooks can only be called at the top level of a functional component, meaning no conditional statements or loops.

  3. Functional Components Only: Hooks can only be called in a React functional components or other hooks — so not in regular JavaScript functions.

These are commonly referred to as the Rules of Hooks.

With these basic rules in mind, let me share a simple example from a project I was working on. Take a look at the following code snippet; I’m sure it’s something many of us have encountered in our own projects before:

const [query, setQuery] = useState("");
const [data, setData] = useState<unknown>(null);

useEffect(() => {
  const timeout = setTimeout(async () => {
	await fetch(`https://example.com?q=${query}`)
  	.then((res) => res.json())
  	.then((data) => setData(data));
  }, 300);

  return () => clearTimeout(timeout);
}, [query]);

This code fetches data from an API when the query changes, with a 300ms delay. It’s a commonly used pattern in React called debouncing, which helps prevent overloading your servers from rapid user input.

Since I often use this pattern in various places in my applications, it became clear that extracting this logic into a custom hook would be beneficial.

Here’s my first attempt at creating a custom hook to encapsulate this logic:

function useDebounceValue<T>(value: T, delay: number): T {
  const [debouncedValue, setDebouncedValue] = useState<T>(value);

  useEffect(() => {
	const handler = setTimeout(() => {
  	setDebouncedValue(value);
	}, delay);
	return () => {
  	clearTimeout(handler);
	};
  }, [value, delay]);

  return debouncedValue;
}

In this hook, I accept a value and a delay as input.

The value is a generic type, which means TypeScript will automatically infer its type based on what is passed in.

Meanwhile, the optional delay parameter determines how long the setTimeout should wait before updating the value.

To utilize my newly created custom hook, I simply import it and call it like any regular JavaScript function:

const [query, setQuery] = useState("");
const [data, setData] = useState<unknown>(null);
const debouncedQuery = useDebounceValue(query, 300);

useEffect(() => {
  void fetch(`https://example.com?q=${query}`)
	.then((res) => res.json())
	.then((data) => setData(data));
}, [debouncedQuery]);

Now, my components are no longer cluttered with repetitive debounce logic. I also realized I could take this a step further by creating a custom hook for the fetch request itself.

This allows me to reuse the fetching logic across multiple components:

const useFetchData = (query: string) => {
  const [data, setData] = useState<unknown>(null);

  useEffect(() => {
	void fetch(`https://example.com?q=${query}`)
  	.then((res) => res.json())
  	.then((data) => setData(data));
  }, [query]);

  return data;
};

Just look at how clean the component code appears now:

const [query, setQuery] = useState("");
const debouncedQuery = useDebounceValue(query, 300);
const data = useFetchData(debouncedQuery);

More importantly, the code inside my component is now streamlined, allowing me to focus on what I want to achieve without getting lost in implementation details.

I also discovered another hidden perk: it’s much easier to make migrations from a single location.

This approach aligns with React's best practices and emphasizes the value of utilizing custom hooks.

It's also good to note if you're building mobile applications, custom hooks work seamlessly in React Native, just as they do for React on the web.

Now that I’ve shared my approach to creating custom React hooks, let's dive deeper into common examples widely used in real-world applications.


Creating a custom data fetching hook

Data fetching is one of the most common operations in front-end development. You should fetch data from an API on page load, on a button click, or when a user scrolls to the bottom of the page. Despite the varied triggers, I've found that the underlying logic often follows a structured pattern:

  1. Accept the API endpoint (URL) as an input parameter.

  2. Manage and return the fetched data.

  3. Track the status of the request, including loading, success, and error state

Typically, I would write this fetching logic directly inside our components, like so:

```typescript

const [data, setData] = useState<unknown | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);

useEffect(() => {
  (async () => {
	try {
  	setLoading(true);
  	const res = await fetch(url);
  	const data = await res.json();
  	setData(data);
	} catch (err) {
  	setError(err as any);
	} finally {
  	setLoading(false);
	}
  })();
}, [url]);

I often found it tedious to repeat this logic for every endpoint I needed to fetch. Each new API call meant yet another useEffect block with multiple useState calls!

Not to mention that I hadn't even implemented any features for aborting ongoing fetch calls or handling refetching.

In light of this, I decided it was best to abstract this stateful logic into a custom hook. This way, I could get the data with just one line of code by invoking the hook with the appropriate URL.

Before diving into implementation, I first defined what I wanted the custom hook’s interface to look like:

function useFetch<T>(
  url: string,
  { autoInvoke, ...options }?: RequestInit & { autoInvoke: boolean },
): {
  data: T | null;
  loading: boolean;
  error: Error | null;
  refetch: () => Promise<any>

Don't worry if this looks a bit overwhelming. I'll break it down for you:

  • The useFetch function accepts a URL and an optional configuration object.

  • The configuration object primarily contains default options for the in-built fetch. It is of type RequestInit from the Fetch API.

  • The configuration object also includes a custom autoInvoke property that I added to determine whether the fetch request should be made automatically.

  • The function returns an object with the fetched data, loading state, error state, and two functions: refetch and abort.

  1. refetch allows you to manually trigger a fetch request.

  2. abort lets you cancel an ongoing fetch request.

With that groundwork established, I implemented the custom hook as follows:

import { useCallback, useEffect, useRef, useState } from "react";

export interface UseFetchOptions extends RequestInit {
  autoInvoke?: boolean;
}

export function useFetch<T>(
  url: string,
  { autoInvoke = true, ...options }: UseFetchOptions = {},
) {
  const [data, setData] = useState<T | null>(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<Error | null>(null);
  const controller = useRef<AbortController | null>(null);

  const refetch = useCallback(() => {
	if (!url) {
  	return;
	}

	if (controller.current) {
  	controller.current.abort();
	}

	controller.current = new AbortController();

	setLoading(true);

	return fetch(url, { signal: controller.current.signal, ...options })
  	.then((res) => res.json())
  	.then((res) => {
    	setData(res);
    	return res as T;
  	})
  	.catch((err) => {
    	if (err.name !== "AbortError") {
      	setError(err);
    	}

    	return err;
  	})
  	.finally(() => {
    	setLoading(false);
  	});
  }, [url]);

  const abort = useCallback(() => {
	if (controller.current) {
  	controller.current.abort("");
	}
  }, []);

  useEffect(() => {
	if (autoInvoke) {
  	refetch();
	}

	return () => {
  	if (controller.current) {
    	controller.current.abort("");
  	}
	};
  }, [refetch, autoInvoke]);

  return { data, loading, error, refetch, abort };
}

In the component where I needed to fetch data, the code can now be simplified to just one line.

const { data, loading, error, refetch, abort } = useFetch<Item[]>(
  "https://example.com/items/",
);

Now, I can fetch data from as many API endpoints as I want without cluttering my UI components. This is the power of custom hooks in React.

I personally use this useFetch hook in many of my projects, and it has become a staple in my codebase. To effectively store and document precious reusable code, I save it as code snippets using Pieces.

Long-term memory helps me save, search, and enrich my code snippets across all my projects.

Whenever I save a code snippet, Pieces automatically adds meta tags, a title, and a descriptive summary explaining what the code does for easy findability. It even scans for any potential sensitive information or Personally Identifiable Information (PII).

With my code snippets organized and easily accessible, I can quickly retrieve and use them whenever I need. You can check out the custom fetch hook we just created as a snippet here.


Creating a custom hook to track the mouse position

I can recall multiple scenarios where I needed to track the mouse position inside a specific DOM element. This could be for a drag-and-drop feature, custom cursor, or other interactive use cases.

In any case, a custom hook that could return the x and y coordinates, allowing me to track the mouse movement effectively,  would have been a godsend.

So, here we go—I’m going to create that hook!

Note: This is a super-simplified version just to get started.

import { MouseEvent, useEffect, useRef, useState } from 'react';

export function useMouse<T extends HTMLElement = any>() {
  const [position, setPosition] = useState({ x: 0, y: 0 });
  const ref = useRef<T>(null);

  const setMousePosition = (event: MouseEvent<HTMLElement>) => {
	if (ref.current) {
  	const rect = event.currentTarget.getBoundingClientRect();
  	const x = Math.max(
    	0,
    	Math.round(event.pageX - rect.left - window.scrollX)
  	);
  	const y = Math.max(
    	0,
    	Math.round(event.pageY - rect.top - window.scrollY)
  	);
  	setPosition({ x, y });
	} else {
  	setPosition({ x: event.clientX, y: event.clientY });
	}
  };

  useEffect(() => {
	const element = ref?.current ? ref.current : document;
	element.addEventListener('mousemove', setMousePosition as any);
	return () => {
  	element.removeEventListener('mousemove', setMousePosition as any);
	};
  }, [ref.current]);

  return { ref, ...position };
}

Here is a brief explanation for the above code:

  • First, I create a useState called position to hold the x and y coordinates of the mouse, initializing them to 0.

  • I then create a ref using useRef to allow me to attach the mouse tracking to a specific DOM element.

  • I define the setMousePosition function, which serves as the event handler for mouse move events. If the refis attached to an element, I calculate the mouse position relative to that element using getBoundingClientRect.The calculations ensure that the coordinates are always non-negative and take into account window scrolling.

  • I use useEffect to add an event listener for mouse movements when the component mounts. This listener calls setMousePosition. If the ref is not attached to any element, I fall back to tracking the mouse position relative to the viewport.

  • When the component unmounts or the ref changes, I clean up by removing the event listener.

Here is how I consume this hook while targeting mouse tracking on a specific <div> element:

  const { ref, x, y } = useMouse<HTMLDivElement>();

  return (
	<div
  	ref={ref}
  	style={{ width: '100%', height: '400px', border: '1px dashed black', position: 'relative' }}
	>
  	<p>Mouse Position: {`X: ${x}, Y: ${y}`}</p>
	</div>
  );

Here, I just call the useMouse hook and attach the ref to the <div> element to track the mouse position inside. The coordinates are displayed on the screen as the mouse moves within the element. 

If I didn’t attach the provided ref to any DOM element, the tracked mouse position would default to being relative to the root document.


Testing custom hooks in React

It's crucial to test the code written in an application to ensure it behaves as expected across different scenarios. When it comes to custom hooks, I test them just like I would test any regular React component. 

In fact, they're often easier to test because they are isolated and functionally pure (meaning they do not produce side effects).

In this section, I'll share the techniques and different tools available I use daily for writing effective tests.

To give a quick refresher, there are three main types of tests: Unit, Integration, and End-to-End (E2E) testing

As you move from Unit to Integration to E2E, the time and resources required to write and maintain these tests increase, but so does your confidence in the application’s behavior. 

Here, I'll primarily focus on unit testing.

Unit testing involves testing small, isolated pieces of code to verify that each unit functions correctly. To assert these behaviors, I typically use a testing framework like Vitest.

Meanwhile, to render custom hooks in my tests, I use React Testing Library.

If you like to learn more about different types of testing, check out 9 Types of API Testing to Ensure Performance and Security and keep an eye on the new one about unit testing llms.

To illustrate a real-world scenario for testing a custom hook, consider my application where I have a hook designed to retrieve an access token for a user on an authenticated route. For this example, let’s call it useAuthState.

The token is stored in local storage, and if it doesn’t exist when the hook is called, a new one is fetched.

Thinking through its use case, there are only three possible states:

  1. The token is present in local storage; it should immediately return the token.

  2. The token is absent; it should fetch a new one.

  3. An error occurs during the fetch; it should return the error.

With this outline in mind, I can begin implementing the test cases. To speed up this process, I'll turn to Pieces Copilot to scaffold the initial test suite.

Pieces Copilot is an extension of Pieces bringing the power of AI directly into my IDE. It saves me time and effort by providing context-aware suggestions tailored to my codebase.

Here's how Iuse Pieces Copilot directly in the editor to generate test cases for the useAuthState hook:

In this case, I asked Pieces Copilot using my current selection (the entire file) to generate unit tests for the useAuthState hook, specifying Vitest as the testing framework and React Testing Library (RTL) for rendering. Pieces provided a series of test cases that validated the hook's behavior.

Here's the code it generated:

import { renderHook, waitFor } from "@testing-library/react";
import { afterEach, describe, expect, it, vi } from "vitest";
import { useAuthState } from "./useAuthState";

const LOCAL_STORAGE_KEY = "accessToken";
const LOCAL_STORAGE_VALUE = "test-token";

describe("useAuthState", () => {
  afterEach(() => {
	window.localStorage.clear();
  });

  it("should load accessToken from localStorage if present", () => {
	window.localStorage.setItem(LOCAL_STORAGE_KEY, LOCAL_STORAGE_VALUE);
	const { result } = renderHook(() => useAuthState());

	expect(result.current.accessToken).toBe(LOCAL_STORAGE_VALUE);
	expect(result.current.isError).toBe(false);
	expect(result.current.error).toBeNull();
  });

  it("should fetch and set accessToken if not in localStorage", async () => {
	const { result } = renderHook(() => useAuthState());

	await waitFor(() => {
  	expect(result.current.accessToken).toBe(LOCAL_STORAGE_VALUE);
  	expect(result.current.isError).toBe(false);
  	expect(result.current.error).toBeNull();
	});
  });

  it("should handle fetch error and set isError to true", async () => {
	const { result } = renderHook(() => useAuthState());

	await waitFor(() => {
  	expect(result.current.isError).toBe(true);
  	expect(result.current.error).toBe("Fetch error");
	});
  });
});

These tests thoroughly validate the hook's behavior across all happy paths, including negative scenarios. To run the test I use:

pnpm vitest run

Upon running the test suite, all tests pass successfully.

Terminal displaying successful test results after running the test suite

However, I’m not done yet — my tests currently depend on an external dependency: the API fetch request.

When testing custom hooks, it's common to encounter situations where you might need to mock external dependencies as you want to control their behavior. 

In this case, I’ll need to mock the fetch API to simulate both success and failure scenarios. I can use the vi.spyOn API provided by Vitest to intercept global fetch calls and return mock values.

Thanks to Pieces long-term memory, I can easily ask it to modify my tests to mock the fetch API. 

This time, I’ll use the Pieces for Developers Desktop App, where my chat history is fully preserved. This highlights how seamlessly Pieces integrates with any development workflow.

Pieces for Developers Desktop App interface showing a chat history and code modifications to mock the fetch API in test cases

Here's the result after updating the tests:

// ...
import { vi } from "vitest";

const mockFetchSuccess = (data: string) =>
  vi.spyOn(global, "fetch").mockResolvedValue({
	json: vi.fn().mockResolvedValue(data),
  } as unknown as Response);

const mockFetchFailure = () =>
  vi.spyOn(global, "fetch").mockRejectedValue("Fetch error");

I’ve added two helper functions: mockFetchSuccess and mockFetchFailure. The former resolves with the provided data, while the latter rejects with an error message.

Next, I called the mocked functions in their respective test cases and restored the original fetch function after each test to avoid any possible side effects.

describe("useAuthState", () => {
  afterEach(() => {
	// ...
	vi.restoreAllMocks();
  });

  it("should load accessToken from localStorage if present", () => {
	// ...
  });

  it("should fetch and set accessToken if not in localStorage", async () => {
	mockFetchSuccess(ACCESS_TOKEN_LOCAL_STORAGE_VALUE);
	// ...
  });

  it("should handle fetch error and set isError to true", async () => {
	mockFetchFailure();
	// ...
  });
});

With these mocks in place, our tests become deterministic. I can run them as many times as I want in any environment without worrying about external API failures. If you're looking to learn more about Unit testing in React, check out this modern guide.


Conclusion

In this article, we explored React hooks, the rules that govern them, and how they serve as a powerful tool for abstracting complex logic. Additionally, we covered how to create our own custom hooks and effectively test them using Pieces Copilot as an assistant.

Pieces can be your development companion – from saving code snippets for future use and generating test cases to chatting with the copilot to help you modify existing code, Pieces can help at every step of the way.

I hope I was able to demonstrate how custom react hooks are a game-changer. If you're looking to learn more, the React official guide is a helpful resource.

Thanks for reading!

Find some time to read these articles too: 

  1. Collaboration tools that you need to know for productive remote work

  2. How to build a server in Dart: CTO insights

  3. Practical API methods & guide to API caching

This article was first published on August, 5th, 2022, and was improved by Emmanuel Isenah as of February 14th, 2025, to improve your experience and share the latest information.

Raman Hundal.
Raman Hundal.

Written by

Written by

SHARE

Creating React custom hooks with Pieces

Title

Title

our newsletter

Sign up for The Pieces Post

Check out our monthly newsletter for curated tips & tricks, product updates, industry insights and more.

our newsletter

Sign up for The Pieces Post

Check out our monthly newsletter for curated tips & tricks, product updates, industry insights and more.

our newsletter

Sign up for The Pieces Post

Check out our monthly newsletter for curated tips & tricks, product updates, industry insights and more.