NeuroAgent

Testing React Router useNavigate Hook: Mock Not Working

Learn why your useNavigate mock isn't being called in React Router tests and discover working solutions for proper hook mocking with TypeScript support.

Question

Why is my mockNavigate function not being called when testing React Router’s useNavigate hook?

I’m trying to test a component that uses React Router’s useNavigate hook, but my mockNavigate function is never called. Here’s my test setup:

javascript
jest.mock('react-router-dom', () => ({
  ...jest.requireActual('react-router-dom'),
  useNavigate: jest.fn()
}));

describe('MyTest wrapper', () => {
  const mockNavigate = jest.fn();

  beforeEach(() => {
    (useNavigate as jest.Mock).mockReturnValue(mockNavigate);
  });

  it('should navigate to correct path', () => {
    // Component setup code
    ...
    await waitFor(() =>
      expect(mockNavigate).toHaveBeenCalledWith(RouterHelpers.somePath())
    );
  });
});

The test is running, but the mockNavigate function is never called. What could be causing this issue, and how can I properly mock the useNavigate hook for testing?

NeuroAgent

The most common reason your mockNavigate function isn’t being called is that the useNavigate hook requires a Router component context, and your mocking approach may not be properly returning the mock function. Additionally, React Router v6 has specific requirements for how the useNavigate hook should be mocked.


Contents


Understanding the Core Issue

The useNavigate hook from React Router v6 has specific requirements that many developers encounter when testing. The primary issue is that useNavigate must be used within the context of a Router component, and when mocking it, you need to ensure your mock implementation properly returns the navigate function.

From Stack Overflow discussions, developers consistently encounter two main problems:

  1. Router Context Error: “useNavigate() may be used only in the context of a Router component”
  2. Mock Implementation Issues: The mock function isn’t properly structured to return what the hook expects

The error navigate is not a function indicates that your mock isn’t returning the expected function structure that useNavigate should provide.


Common Mocking Problems and Solutions

Problem 1: Incorrect Mock Structure

Your current approach has a structural issue. The useNavigate hook should return a navigate function directly, not be mocked to return a mock function that needs to be cast.

javascript
// This is problematic - you're mocking the hook to return a mock function
(useNavigate as jest.Mock).mockReturnValue(mockNavigate);

Problem 2: Module Mocking Conflicts

When you mock the entire module, you need to ensure you’re not overriding the actual module implementation in a way that breaks the expected return structure.

From GitHub discussions, developers found that a proper structure requires returning an object with the navigate function:

javascript
jest.mock('react-router-dom', () => ({
  ...jest.requireActual('react-router-dom'),
  useNavigate: () => ({ navigate: jest.fn() }),
}));

Working Mocking Approaches

Solution 1: Full Module Mock with Proper Structure

This approach mocks the entire react-router-dom module and returns the navigate function in the correct structure:

javascript
jest.mock('react-router-dom', () => ({
  ...jest.requireActual('react-router-dom'),
  useNavigate: () => ({ navigate: jest.fn() }),
}));

describe('MyTest wrapper', () => {
  it('should navigate to correct path', () => {
    // Your component setup here
    const { result } = renderHook(() => useNavigate());
    
    // Call the navigate function from the mock
    result.current.navigate('/some-path');
    
    // Now the mock will be called
    expect(result.current.navigate).toHaveBeenCalledWith('/some-path');
  });
});

Solution 2: Spy Approach

This approach spies on the actual useNavigate function and mocks its implementation:

javascript
import * as router from 'react-router-dom';

describe('MyTest wrapper', () => {
  const mockNavigate = jest.fn();
  
  beforeEach(() => {
    jest.spyOn(router, 'useNavigate').mockImplementation(() => mockNavigate);
  });

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

  it('should navigate to correct path', () => {
    // Component setup code
    render(<YourComponent />);
    
    // Trigger the navigation
    userEvent.click(screen.getByRole('button'));
    
    expect(mockNavigate).toHaveBeenCalledWith('/some-path');
  });
});

Solution 3: Component Wrapping with MemoryRouter

For component-level testing, wrap your component with a MemoryRouter to provide the necessary context:

javascript
import { MemoryRouter } from 'react-router-dom';

describe('MyTest wrapper', () => {
  it('should navigate to correct path', () => {
    render(
      <MemoryRouter>
        <YourComponent />
      </MemoryRouter>
    );
    
    userEvent.click(screen.getByRole('button'));
    
    // You can test navigation by checking the rendered result
    expect(screen.getByText('Expected Page Content')).toBeInTheDocument();
  });
});

Testing Strategies for Different Scenarios

Testing Custom Hooks with useNavigate

When testing custom hooks that use useNavigate, use renderHook from @testing-library/react-hooks:

javascript
import { renderHook } from '@testing-library/react-hooks';
import { useNavigate } from 'react-router-dom';

jest.mock('react-router-dom', () => ({
  ...jest.requireActual('react-router-dom'),
  useNavigate: () => ({ navigate: jest.fn() }),
}));

describe('useCustomHook', () => {
  it('should navigate when condition is met', () => {
    const { result } = renderHook(() => useCustomHook());
    
    result.current.handleAction();
    
    expect(result.current.navigate).toHaveBeenCalledWith('/expected-path');
  });
});

Testing Components with Navigation

For full component testing, combine MemoryRouter with your mock:

javascript
import { MemoryRouter } from 'react-router-dom';

jest.mock('react-router-dom', () => ({
  ...jest.requireActual('react-router-dom'),
  useNavigate: () => ({ navigate: jest.fn() }),
}));

describe('MyComponent', () => {
  it('navigates on button click', () => {
    render(
      <MemoryRouter>
        <MyComponent />
      </MemoryRouter>
    );
    
    const button = screen.getByRole('button');
    userEvent.click(button);
    
    // Verify navigation by checking route changes
    // or by directly checking the mock
    const navigateMock = require('react-router-dom').useNavigate();
    expect(navigateMock.navigate).toHaveBeenCalledWith('/target-path');
  });
});

Best Practices and Troubleshooting

1. Always Verify Router Context

Ensure your component is within a Router context when testing:

“If any of the components you are rendering in your test use the useNavigate hook, you have to wrap them in a Router when testing them.” - bobbyhadz.com

2. Use Proper Mock Structure

The useNavigate hook should return an object with a navigate function:

javascript
// Correct structure
useNavigate: () => ({ navigate: jest.fn() })

// Incorrect structure that causes issues
useNavigate: jest.fn()

3. Debug Your Mocks

Add console logs to verify your mock is being called:

javascript
beforeEach(() => {
  const mockNavigate = jest.fn().mockImplementation((path) => {
    console.log('Mock navigate called with:', path);
  });
  
  jest.spyOn(router, 'useNavigate').mockImplementation(() => mockNavigate);
});

4. Handle TypeScript Casting Properly

If using TypeScript, ensure proper typing:

typescript
import { useNavigate } from 'react-router-dom';

jest.mock('react-router-dom', () => ({
  ...jest.requireActual('react-router-dom'),
  useNavigate: jest.fn() as jest.Mock<() => (path: string) => void>,
}));

5. Alternative: Test Navigation Behavior Instead of Implementation

Consider testing the actual navigation behavior rather than mocking the hook:

javascript
import { MemoryRouter } from 'react-router-dom';
import { RouterProvider } from 'react-router-dom';

describe('MyComponent', () => {
  it('navigates correctly', () => {
    render(
      <MemoryRouter initialEntries={['/']}>
        <RouterProvider router={testRouter}>
          <MyComponent />
        </RouterProvider>
      </MemoryRouter>
    );
    
    userEvent.click(screen.getByRole('button'));
    
    // Check that the URL has changed
    expect(screen.getByText('Target Page Content')).toBeInTheDocument();
  });
});

Conclusion

The issue with your mockNavigate function not being called typically stems from one of these problems:

  1. Incorrect mock structure - Ensure useNavigate returns { navigate: jest.fn() }
  2. Missing router context - Wrap components in MemoryRouter for component tests
  3. Timing issues - Set up mocks before rendering components
  4. TypeScript casting problems - Use proper mock typing

The most reliable approach is to use the full module mocking with proper structure and combine it with MemoryRouter for component testing. For hook-specific tests, use renderHook with the same mocking pattern.

Remember that React Router v6 has different requirements than v5, and the useNavigate hook specifically needs to be mocked to return an object with a navigate function property. Following these patterns will ensure your navigation tests work reliably and predictably.