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:
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?
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
- Common Mocking Problems and Solutions
- Working Mocking Approaches
- Testing Strategies for Different Scenarios
- Best Practices and Troubleshooting
- Conclusion
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:
- Router Context Error: “useNavigate() may be used only in the context of a Router component”
- 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.
// 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:
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:
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:
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:
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:
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:
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:
// 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:
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:
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:
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:
- Incorrect mock structure - Ensure useNavigate returns
{ navigate: jest.fn() } - Missing router context - Wrap components in MemoryRouter for component tests
- Timing issues - Set up mocks before rendering components
- 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.