Skip to content

ver0-project/react-hooks-testing

Repository files navigation

@ver0/react-hooks-testing

NPM Version NPM Downloads Dependents (via libraries.io), scoped npm package GitHub Actions Workflow Status Codecov NPM Type Definitions


🧪 Test your React hooks with ease!

Features

  • ✅ Supports React 19.
  • ✅ Server-Side Rendering (SSR) support.
  • ✅ Easy-to-use API.
  • ✅ Errors reporting, even for SSR.
  • ✅ Compatible with any testing framework.

Why this package exists?

In May 2022, @testing-library/react-hooks was announced as deprecated, leaving its users locked to React 17. The so-called "merge" with @testing-library/react was never fully realized, as the provided renderHook offers only a fraction of the functionality that @testing-library/react-hooks had, and it completely lacks SSR support.

I had hoped that the release of React 19 and server components would bring changes, but nothing has improved despite React 19 being in RC for almost a year before its release. When asking about SSR support, the response is often to resolve the issues independently.

This package was created to meet the needs of the hooks library I have been developing. I encourage everyone to contribute and help make hooks testing simple again. This package offers improved typings compared to @testing-library/react-hooks, is testing-framework agnostic, supports SSR, and is easy to use.

Dependencies and compatibility

This package aims to be compatible only with the current major version of React. Additionally, it encourages the use of newer React APIs, such as completely ditching synchronous rendering and support of synchronous act behavior, as React documentation states it will be deprecated in future releases.

How to use

Since the library is designed to be testing-framework agnostic, it does not have any automatic setup and therefore requires a tiny bit of pre-configuration to work with your testing framework. I'm using vitest and will provide examples for it, but all known frameworks have similar setup functionality.

As this library is tested through testing other hooks, following configuration is applied to this repository and can be used as an example.

Setup

After adding @ver0/react-hooks-testing to your dev-dependencies, create a file with the following content and add it to your test runner configuration as a global setup file.

import {hooksCleanup} from "@ver0/react-hooks-testing";
import {afterEach} from 'vitest';

afterEach(hooksCleanup);
import {defineConfig} from 'vitest/config';

export default defineConfig({
	test: {
		dir: './src',
		setupFiles: [
			'./src/tests/setup/react-hooks.test.ts',
		],
	},
});

Or, if you don't have many hooks to test, add it directly to your test files.

All this code does - unmounts all rendered hooks after each test, so you don't have to worry about it yourself.

Testing client-side hooks

renderHook function made close to @testing-library/react API-wise, but with several differences, that are dictated by act function.

The act function provided by this library is similar to the one from react, but with stricter typings. It only allows async functions to be passed to it, ensuring it always returns a promise, which should be awaited. This is in line with the React documentation, which states that synchronous act will be deprecated in future releases.

Additionally, this act function bypasses the console error regarding the testing environment configuration. Since you're using this library to test hooks, you're already in a testing environment, making this error redundant.

import {expect, test} from "vitest";
import {act, renderHook} from "@ver0/react-hooks-testing";

test('should use setState value', async () => {
	const {result} = await renderHook(() => useState('foo'))

	expect(result.value).toBe("foo")
	expect(result.error).toBe(undefined)

	if (result.value === undefined) {
		return;
	}

	await act(async () => {
		result.value[1]('bar')
	})

	expect(result.value[0]).toBe('bar')
})

As you can see in above example - renderHook function is asynchronous, reason for that is React's request to perform any rendering within the act function.

In contrast to @testing-library/react, result object has the following type definition:

/**
 * Represents the result of rendering a hook.
 */
export type ResultValue<T> = {
	readonly value: undefined;
	readonly error: Error;
} | {
	readonly value: T;
	readonly error: undefined;
}

/**
 * Represents the last rendered hook result and all previous results.
 */
export type ResultValues<T> = ResultValue<T> & {
	readonly all: ResultValue<T>[];
}

The render results history is accessible via the all property, which contains every rendering result as well as the value of the latest render. Unlike @testing-library/react, which collects results during the effect phase, this library populates the results during the render phase - it allows to ensure tested hook results correctness throughout each render.

Another notable difference is the error property. This property captures any error thrown during the hook’s rendering, thanks to an Error Boundary component wrapped around the hook’s harness component.

Each result object is immutable and contains either a value or an error property—but never both. The hook’s result object follows the same principle. Although the value and error properties are implemented as getters and always exist, the values they return correspond to the most recent render result from the all array.

Otherwise, the API is similar to @testing-library/react. Note that the waitForNextUpdate function is not provided, as modern testing frameworks include their own waitFor function, which serves the same purpose.

Testing server-side hooks

The primary purpose of this package is to provide out-of-the-box SSR (Server-Side Rendering) support through the renderHookServer function. This function works similarly to renderHook but includes a few key differences:

  • Initial Render: The initial render is performed using React's renderToString function.
  • Hydration: The hook's render result includes an extra hydrate function that hydrates the hook after the initial render.
  • DOM Root Creation: The client-side DOM root is created only when the hydrate function is called, facilitating testing in a server environment.
  • Rerenders: Rerenders are only possible after hydration.

Otherwise, usage is similar to renderHook, so dedicated examples are not provided. For usage examples, refer to the *.ssr.test.ts files in this repository.