How to Test React Hooks

A version of this article is also avaiable on Medium.

React hooks are an essential part of building React applications. They provide a clean way to manage state and side effects. Testing these hooks ensures that your logic is solid, but in Luau, writing tests for hooks is less fun due to the lack of a renderHook utility in the Luau version of react-testing-library.

To help address this gap, I’ve created a small package that introduces a renderHook function, inspired by its JavaScript counterpart.

Keep reading to see how easy it is to write tests for your hooks.

Testing a Custom Hook

To demonstrate, let’s work with a simple useCounter hook:

local function useCounter(initialValue: number?)
    initialValue = initialValue or 0

    local count, setCount = React.useState(initialValue)

    local function increment()
        setCount(function(prevCount: number)
            return prevCount + 1
        end)
    end

    local function decrement()
        setCount(function(prevCount: number)
            return math.max(0, prevCount - 1)
        end)
    end

    local function reset()
        setCount(initialValue or 0)
    end

    return { 
	    count = count, 
	    increment = increment, 
	    decrement = decrement, 
	    reset = reset,
	}
end

In the following steps, test cases will be built using jest. If you are not familiar with it, it should still be pretty easy to follow.

Step 1: Testing the default initial value

The renderHook function initializes the hook and gives us access to its current state. For example, we can test the initial state of useCounter like this:

local renderHook = require('@pkg/@seaofvoices/react-render-hook').renderHook

it("initializes the counter at 0 by default", function()
	local renderResult = renderHook(useCounter)

	local counterRef = renderResult.result

	expect(counterRef.current.count).toEqual(0)
end)

The renderHook function gives back a React ref with what the hook returned with. In this case, the renderResult.result ref contains the counter object returned by useCounter.

Step 2: Testing for a different initial value

To test if the initial value can be customized, simply provide arguments to the hook when calling renderHook. These arguments are passed directly to the hook:

it("initializes the counter at 100", function()
	-- any values after useCounter will be passed to the hook
	local renderResult = renderHook(useCounter, 100)

	local counterRef = renderResult.result

	expect(counterRef.current.count).toEqual(100)
end)

Step 3: Testing for re-renders with different initial values

To make sure re-rendering the hook does not change its state, the renderResult.rerender can be called with new arguments to simulate an update.

it("keeps the initial value even after re-rendering with a new value", function()
	local renderResult = renderHook(useCounter, 100)

	local counterRef = renderResult.result

	expect(counterRef.current.count).toEqual(100)

	-- re-render the hook with a different value
	renderResult.rerender(25)

	expect(counterRef.current.count).toEqual(100)
end)

Step 4: Testing the increment function

To test the useCounter hook properly, we need to simulate calls to increment. For example:

local ReactTesting = require('@pkg/@jsdotlua/react-testing-library')

it("increments the counter", function()
	local renderResult = renderHook(useCounter)
	local counterRef = renderResult.result

	ReactTesting.act(function()
		counterRef.current.increment()
	end)

	expect(counterRef.current.count).toEqual(1)
end)

If you are not familiar with ReactTesting.act: this ensures that React processes the increment state change before the test continues. Without it, the state change would not complete and the test would fail.

Note: to enable act, you will need to set the global variable __ROACT_17_MOCK_SCHEDULER__ to true before requiring React.

Setup

The project is available on npm, so you can easily add it with your favorite npm compatible package manager:

yarn add -D @seaofvoices/react-render-hook
npm install @seaofvoices/react-render-hook --save-dev

Now there is one more tool in your toolbox at your disposition. If you have other questions, leave a reply to this thread.

End Notes

I work part time to dedicate time to the Luau open-source ecosystem. If you appreciate this library or other stuff I built, please consider leaving a tip :sparkling_heart:I have a page on ko-fi where you can contribute or GitHub sponsors are available in any of my projects.

Useful Links

As usual, I like ending my posts with a list of various resources:

5 Likes