How to Mock Client-Side Rendering in React

Testing React components that rely on client-side rendering (CSR) often requires simulating browser-specific environments and APIs in Node.js-based test runners like Jest or Vitest. This article explains how to mock CSR behavior, simulate browser globals such as window and document, and mock client-only hooks to ensure your components test successfully without a real browser.

Mocking Browser Globals

When running tests in a Node environment, browser-specific APIs like window.matchMedia, localStorage, or sessionStorage are either missing or incomplete. To test CSR components that rely on these APIs, you must mock them globally.

You can define these mocks directly in your test file or in a global setup file (e.g., setupTests.js):

// Mocking window.matchMedia
Object.defineProperty(window, 'matchMedia', {
  writable: true,
  value: jest.fn().mockImplementation(query => ({
    matches: false,
    media: query,
    onchange: null,
    addListener: jest.fn(), // Deprecated
    removeListener: jest.fn(), // Deprecated
    addEventListener: jest.fn(),
    removeEventListener: jest.fn(),
    dispatchEvent: jest.fn(),
  })),
});

// Mocking localStorage
const localStorageMock = (() => {
  let store = {};
  return {
    getItem: (key) => store[key] || null,
    setItem: (key, value) => { store[key] = value.toString(); },
    clear: () => { store = {}; },
    removeItem: (key) => { delete store[key]; }
  };
})();

Object.defineProperty(window, 'localStorage', { value: localStorageMock });

Mocking Client-Only Execution

Some React applications use conditional checks like typeof window !== 'undefined' or custom hooks to defer rendering until the component has mounted on the client.

If you want to force your tests to simulate either a client-side environment or a server-side environment, you can mock the window object dynamic state.

To mock a custom hook that detects CSR, such as useIsMounted:

// useIsMounted.js
import { useState, useEffect } from 'react';

export function useIsMounted() {
  const [mounted, setMounted] = useState(false);
  useEffect(() => {
    setMounted(true);
  }, []);
  return mounted;
}

In your test, you can mock this hook to instantly return true to simulate a post-mount client state:

import { render, screen } from '@testing-library/react';
import { useIsMounted } from './useIsMounted';
import MyComponent from './MyComponent';

// Mock the hook hook module
jest.mock('./useIsMounted', () => ({
  useIsMounted: jest.fn(),
}));

test('renders client-only content', () => {
  // Force the hook to return true (simulating CSR mounted state)
  useIsMounted.mockReturnValue(true);

  render(<MyComponent />);
  expect(screen.getByText(/client-only features/i)).toBeInTheDocument();
});

Mocking Client-Side Data Fetching

Client-side rendering frequently relies on fetching data immediately after mounting via useEffect or libraries like TanStack Query. To test these components without making actual network requests, you must mock the global fetch API.

describe('Client-Side Fetching Component', () => {
  beforeEach(() => {
    global.fetch = jest.fn().mockImplementation(() =>
      Promise.resolve({
        json: () => Promise.resolve({ data: 'Mocked Client Data' }),
      })
    );
  });

  afterEach(() => {
    jest.clearAllMocks();
  });

  test('fetches and displays data on client mount', async () => {
    render(<DataComponent />);
    const dynamicContent = await screen.findByText('Mocked Client Data');
    expect(dynamicContent).toBeInTheDocument();
  });
});