/

Engineering

Feb 15, 2025

Feb 15, 2025

Creating my first 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

To create a custom React hook, there are a few restrictions to keep in mind:

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

  2. They can only be called at the top level of a functional component, meaning no conditional statements or loops.

  3. They 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’s walk through a simple example. Imagine in an application, we need to grab the access token for a user in an authenticated route from local storage. If the token doesn't exist, we'll want to fetch a new one.

Our first instinct might be to write the logic directly in the component that needs it (which works fine). But since the authentication state is so important and will likely be accessed in many parts of your application, repeating the logic becomes redundant.

Instead, we can create a custom hook called useAuthState and move all the duplicated code into it.

A code snippet with the React code for a custom hook.

Save to Pieces

In this `useAuthState` hook, we can observe the following:

  • The useState hook stores the access token and error state.

  • The useEffect hook checks for an access token in local storage as soon as the component mounts. If none is found, it makes a fetch request to retrieve a new one.

To use the custom hook, we simply import it and call it like any regular JavaScript function:

A code snippet that implements the above custom hook.

By using this custom hook, our components can:

  1. Focus on what they want to achieve in a declarative manner rather than on the implementation details.

  2. Enable easy adjustments or data transformations from a single location.

This approach aligns with React 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 we have a basic understanding of custom hooks, let’s dive deep into some more React custom hooks examples.


Creating a custom data fetching hook

Data fetching is a common operation in front-end development. We might need to 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, the underlying logic often follows a structured pattern:

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

  2. Manage and return the fetched data.

  3. Track the request status, such as loading, success, or error states.

Typically, we'd write this logic directly inside our components, like so:

A code snippet that creates another custom hook.

Save to Pieces

We would use multiple useState and useEffect calls to get the fetched data, resulting in a clustered component. Imagine we were to repeat this multiple times across multiple components - it would be a hassle to maintain, not to mention refactor.

In such case, it is suitable for us to abstract such stateful logic into a custom hook. Then we can call the hook with the URL to get the data and statuses.

Extracting the above code into a custom hook, we would have:

A code snippet with React code.

Save to Pieces

The above code may look a bit confusing, so let's break down the changes:

  • First, we wrapped the entire logic inside a function called useFetch.

  • We introduced a setTimeout inside the useEffect block with a 2000 millisecond delay to simulate a network request (note: this is for demonstration only and should not be used in production).

  • Lastly, we returned the fetched data along with loading and error states.

The code below demonstrates how we can consume this hook:  

A code snippet with React code.

Save to Pieces

Now, our data fetching logic is decoupled from our UI. If we want to take things a step further, we could improve this hook by using TypeScript generics to dynamically infer the data type instead of casting it as unknown. However, that's beyond the scope of this topic.


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 along with a handler to manage the mouse events would have been a godsend.

So, here we go—we’re going to create it! 

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

A code snippet with React code.

Save to Pieces

Here is a bit of explanation for the above code:

  • First, it has a useState as mouseCoords we default x and y values of 0. This state stores the x and y coordinates of the mouse.

  • Next, we have a handleMouseMovement function. This function is an event handler on mouse move events.

  • Then, we have a useEffect that adds an event listener on page load for the event mouse move. We remove this event listener 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 all of the 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 I consume this hook while targeting mouse tracking on a specific div element:

A React snippet that consumes custom hooks.

Save to Pieces

Creating a custom hook to update CSS variables

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

In large projects, we can repeat styles, colors, or sizes in 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 for us to have a central place to store these values and use them later. We can think of this as a central repository of style properties. These variables once created can be accessed anywhere in the document via CSS’s var() function when we declare them inside the :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 create 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:

A code snippet with CSS code.

Save to Pieces

Simple usage of these variables would be similar to below:

A CSS code snippet.

Save to Pieces

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

We will need to do the  following:

  • Create a state variable inside a component

  • Create a use effect 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.

A React code snippet that creates custom hooks.

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 that stores the CSS variables.

  • Next, we create two state variables: cssRootStyles and rootElement.

  • cssRootStyles is a state variable that stores all of 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.

  • Next, we 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 we can use the consumer component once it’s mounted.

A React code snippet.

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, we created a simple button that creates a new CSS variable --secondary-color with the value #CE8147 when it’s clicked.


Testing custom hooks in React

It's crucial to test our code to ensure it behaves as expected across different scenarios. When it comes to custom hooks, we can test them just like we 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, we'll go over techniques and different tools available for writing effective tests.

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. 

We'll primarily focus on unit testing here.

Unit testing involves testing small, isolated pieces of code to verify that each unit functions correctly. To assert behaviors, we'll need a testing framework like Vitest

Additionally, to render custom hooks in tests, we'll use React Testing Library.

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.

Returning to our custom hook `useAuthState`, we'll create a series of tests to ensure it works properly.

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, we can begin implementing the test cases. To speed up the process, we can use AI tools like Pieces to generate test cases, saving valuable time and effort. Pieces Copilot, which retains long-term memory of your codebase, provides context-aware suggestions to help streamline development.

Here's an example of how we can use Pieces Copilot directly in the IDE 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 validate 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. We can run the test suite using:

pnpm vitest run

Upon running the test suite, we should see all tests pass successfully.

Terminal displaying successful test results after running the test suite

However, we're not done yet—our tests currently depend on an external dependency: the API fetch request.

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

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

Thanks to Pieces long-term memory, we can easily ask it to modify our tests to mock the fetch API. This time, we'll use the Pieces for Developers Desktop App, where our 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");

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

Next, we called these mocked functions in their respective test cases. We also 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. We can run them as many times as we 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 my first 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.