How to Mock React Keys in React

In React, the key prop is a special attribute used by the reconciliation algorithm to identify which items in a list have changed, been added, or been removed. Because React consumes the key prop internally, it is not passed down to the component’s actual props, making it tricky to mock or assert on directly. This article explains how to test that keys are correctly assigned to React elements and how to simulate key changes to test component re-mounting behavior using Jest and React Testing Library.

Why You Cannot Read props.key

React reserves the key prop for its virtual DOM diffing process. If you try to access props.key inside a child component, it will return undefined.

To mock or test keys, you must inspect the React elements before they are fully rendered to the DOM, or wrap your components in a way that allows you to trigger key changes.

Asserting That Correct Keys Are Assigned

To verify that your list items are receiving the correct keys, you can inspect the virtual DOM structure using react-test-renderer or by directly checking the returned React elements.

Here is an example using react-test-renderer to assert that keys are correctly assigned to list items:

import React from 'react';
import TestRenderer from 'react-test-renderer';

function TodoList({ items }) {
  return (
    <ul>
      {items.map(item => (
        <li key={item.id}>{item.text}</li>
      ))}
    </ul>
  );
}

test('should assign correct keys to list items', () => {
  const items = [{ id: 'todo-1', text: 'Learn React' }, { id: 'todo-2', text: 'Write Tests' }];
  const renderer = TestRenderer.create(<TodoList items={items} />);
  const root = renderer.root;

  const listItems = root.findAllByType('li');

  // Assert that the keys match the item IDs
  expect(listItems[0].key).toBe('todo-1');
  expect(listItems[1].key).toBe('todo-2');
});

Simulating Key Changes to Force Re-mounting

Sometimes you need to test if a component resets its internal state when its key changes. Since React completely unmounts and remounts a component when its key changes, you can test this behavior in React Testing Library by rerendering the component with a new key.

Here is how you can mock and test this behavior:

import React, { useState } from 'react';
import { render, screen, fireEvent } from '@testing-library/react';

function Counter() {
  const [count, setCount] = useState(0);
  return (
    <div>
      <span data-testid="count">{count}</span>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}

test('should reset state when the key prop changes', () => {
  // 1. Render the component with an initial key
  const { rerender } = render(<Counter key="initial-key" />);
  
  const incrementButton = screen.getByText('Increment');
  const countSpan = screen.getByTestId('count');

  // 2. Increment the counter to change the internal state
  fireEvent.click(incrementButton);
  expect(countSpan.textContent).toBe('1');

  // 3. Rerender the component with a different key
  rerender(<Counter key="new-key" />);

  // 4. Assert that the component unmounted, remounted, and state was reset
  expect(countSpan.textContent).toBe('0');
});

By inspecting the element tree with a test renderer or utilizing React Testing Library’s rerender method to change keys dynamically, you can effectively mock and verify key-dependent behavior in your React applications.