Hone logo
Hone
Problems

Implement a renderHook Utility for React Testing Library with Jest

React Testing Library (RTL) provides powerful tools for testing React components. A common pattern when testing custom React hooks is to render them in isolation to observe their behavior. This challenge asks you to create a utility function, similar to renderHook from RTL, that allows you to test custom hooks effectively within a Jest environment using TypeScript.

Problem Description

Your task is to create a TypeScript function renderHook that takes a callback function (which should return a React component or null) and optional rendering options. This utility should render the provided hook, allow you to interact with it through its returned values, and provide a way to re-render the hook with new props or state.

Key Requirements:

  1. Render the Hook: The renderHook function should accept a callback function that defines the hook to be rendered. This callback can return JSX or null.
  2. Return Value: The renderHook function should return an object containing:
    • result: An object with a current property that holds the latest value returned by the hook.
    • rerender: A function to re-render the hook with new parameters.
    • unmount: A function to unmount the hook.
  3. State Management: The result.current should always reflect the most up-to-date value returned by the hook.
  4. TypeScript Support: The utility should be written in TypeScript and provide strong typing for the hook's return value and the renderHook function's parameters and return object.
  5. Integration with Jest: Assume this utility will be used within a Jest test environment, leveraging React's test renderer or a similar mechanism.

Expected Behavior:

  • When renderHook is called, the hook's callback should execute, and its return value should be accessible via result.current.
  • Calling rerender with new arguments should cause the hook's callback to re-execute with the new arguments, and result.current should update accordingly.
  • Calling unmount should clean up the rendered hook.

Edge Cases:

  • Hooks that return complex objects or primitives.
  • Hooks that rely on context. (For simplicity in this challenge, assume context is not a primary concern, but be mindful of its potential impact).
  • Hooks that perform side effects.

Examples

Example 1: Basic Hook

import React, { useState } from 'react';

function useCounter(initialValue: number = 0) {
  const [count, setCount] = useState(initialValue);
  const increment = () => setCount(prev => prev + 1);
  return { count, increment };
}

// Usage within a test:
// const { result, rerender } = renderHook(useCounter, { initialProps: 5 });
// expect(result.current.count).toBe(5);
// result.current.increment();
// expect(result.current.count).toBe(6);

Input to renderHook: useCounter with initialProps: 5

Expected result.current after initial render: { count: 5, increment: [Function] }

Explanation: The useCounter hook is rendered with an initial value of 5. result.current correctly reflects the initial state. Calling increment updates the state, and result.current.count reflects the change.

Example 2: Hook returning a primitive

import React, { useState } from 'react';

function useToggle(initialState: boolean = false) {
  const [isOn, setIsOn] = useState(initialState);
  const toggle = () => setIsOn(prev => !prev);
  return { isOn, toggle };
}

// Usage within a test:
// const { result, rerender } = renderHook(useToggle);
// expect(result.current.isOn).toBe(false);
// result.current.toggle();
// expect(result.current.isOn).toBe(true);
// rerender(true);
// expect(result.current.isOn).toBe(true);

Input to renderHook: useToggle with initialProps: undefined (defaults to false)

Expected result.current after initial render: { isOn: false, toggle: [Function] }

Expected result.current after result.current.toggle(): { isOn: true, toggle: [Function] }

Expected result.current after rerender(true): { isOn: true, toggle: [Function] }

Explanation: The useToggle hook is rendered. Toggling it correctly updates the isOn state. rerender(true) is used to provide a new initial state to the hook, and the state correctly reflects this new initial value.

Example 3: Hook with no arguments

import React from 'react';

function useLocalStorage(key: string, initialValue: string) {
  const [value, setValue] = React.useState(() => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      console.error(error);
      return initialValue;
    }
  });

  const setItem = (newValue: string) => {
    try {
      setValue(newValue);
      window.localStorage.setItem(key, JSON.stringify(newValue));
    } catch (error) {
      console.error(error);
    }
  };

  return { value, setItem };
}

// Usage within a test (mocking localStorage):
// const mockLocalStorage = { getItem: jest.fn(), setItem: jest.fn() };
// Object.defineProperty(window, 'localStorage', { value: mockLocalStorage });
// const { result, rerender } = renderHook(useLocalStorage, { initialProps: ['myKey', 'defaultValue'] });
// expect(result.current.value).toBe('defaultValue');
// result.current.setItem('newValue');
// expect(result.current.value).toBe('newValue');
// expect(mockLocalStorage.setItem).toHaveBeenCalledWith('myKey', '"newValue"');

Input to renderHook: useLocalStorage with initialProps: ['myKey', 'defaultValue']

Expected result.current after initial render: { value: 'defaultValue', setItem: [Function] }

Expected result.current after result.current.setItem('newValue'): { value: 'newValue', setItem: [Function] }

Explanation: This example demonstrates a hook that interacts with browser APIs. The renderHook should facilitate testing such hooks by allowing the hook's logic to execute. The rerender function can be used to pass different arguments to the hook if its signature changes or if you need to simulate different scenarios.

Constraints

  • The renderHook function should be implemented using React's test-renderer or a similar testing utility that doesn't rely on a full DOM environment.
  • The solution must be in TypeScript.
  • The performance of renderHook itself is not a primary concern for this challenge, but it should be reasonably efficient for typical testing scenarios.
  • You are allowed to use third-party libraries for React's test rendering if necessary (e.g., @testing-library/react-hooks is the inspiration, but you should implement your own version). However, the prompt implies building the core logic yourself.

Notes

  • Consider how you will manage the state updates and ensure result.current is always accurate.
  • Think about how rerender will accept arguments and pass them to your hook's callback.
  • The initialProps passed to renderHook should be the arguments that the hook's callback expects.
  • You'll need to import React and potentially a testing renderer.
  • For result.current to update correctly, you'll likely need to leverage some form of asynchronous update mechanism or force re-renders.
Loading editor...
typescript