Editor’s note: This article was last updated 28 April 2022 to remove references to the deprecated useHistory
Hook.Â
In version 6, React Router introduced a new family of Hooks that have simplified the process of making components route-aware. In this article, we’ll explore these Hooks, looking at a few code examples and use cases. Let’s get started!
useNavigate
- Sample application
- Centralize the history object
- Create a higher-order component to wrap any component under test in a Router
- Test components with
useNavigate
- Testing Hooks
useNavigate
In React Router v6, the useNavigate
Hook replaced the useHistory
Hook. You can use the useNavigate
Hook to navigate to other pages, as seen in the code block below:
import { useNavigate } from "react-router-dom"; function HomeButton() { let navigate = useNavigate(); function handleClick() { navigate("/home"); } return ( <button type="button" onClick={handleClick}> Go home </button> ); }
Testing the useNavigate
Hook with jest.mock
My initial research into testing the useNavigate
Hook returned this StackOverflow thread, which advocates for using jest.mock
. Let’s try it out:
jest.mock('react-router-dom', () => ({ ...jest.requireActual('react-router-dom'), useNavigate: () => (jest.fn()) }));
Although the above approach works, I try to avoid it at all costs since I want my tests to simulate real world usage. jest.mock
will blitz an essential component from the simulated reality, and I might miss some critical regressions. Instead, I’ll lean heavily on React Router’s MemoryHistory
.
Sample application
I’ve created the CodeSandbox below:
It includes a simple Hook called useStepper
that allows the user to navigate forward and backwards through several application steps:
Each forward or backward navigation uses the navigate()
function returned from useNavigate
to navigate to a new component at a new URL:
export const useStepper = () => { const navigate = useNavigate(); const location = useLocation(); const nextStepAction = useCallback(() => { setCurrentStep(index + 1); }, [getCurrentStepIndex]); const previousStepAction = useCallback(() => { setCurrentStep(index - 1); }, [getCurrentStepIndex]); useEffect(() => { const { path } = currentSteps[currentStep]; navigate( path, {state: { previousPath: location.pathname }} ); }, [currentStep, navigate]); // rest };
We can follow a few simple steps to take control of useNavigate
without jest.mock
.
Centralize the history object
First, I centralize all access to the history
object into a single export from one file located at src/history/index.ts
:
import { createBrowserHistory, createMemoryHistory } from "history"; import { Urls } from "../types/urls"; const isTest = process.env.NODE_ENV === "test"; export const history = isTest ? createMemoryHistory({ initialEntries: ['/'] }) : createBrowserHistory();
With this approach, I guarantee that all test and application code is dealing with the same history
object. I usually keep conditionals like process.env.NODE_ENV === "test";
out of the application code, but I’m making an exception in this case.
Create a higher-order component to wrap any component under test in a Router
As mentioned previously, the import below will resolve to the central export that both the application code and test code now reference:
import { history } from "../history"; import React from "react"; import { render } from "@testing-library/react"; import { Router } from "react-router-dom"; export const renderInRouter = (Comp: React.FC) => render( <Router history={history}> <Comp /> </Router> ); renderInRouter is a simple function that takes a component and wraps it in a router. The critical thing to note here is the import in line 1: javascript import { history } from "../history";
Test components with useNavigate
With the two previous steps completed, testing components that use useNavigate
will be easy.
To set up the test scenarios, the useStepper.test.tsx
test references the same history
object as the application code:
import React from "react"; import { history } from "../../history"; import { useStepper } from "./useStepper"; import { Urls } from "../../types/urls"; import { renderInRouter } from "../../tests"; import { renderHook, act } from "@testing-library/react-hooks"; import { ApplicationNavigator } from "../../Containers/Application"; import { screen, fireEvent } from "@testing-library/react"; import { Router } from "react-router-dom"; const render = () => renderInRouter(ApplicationNavigator); describe("useStepper", async () => { beforeEach(() => { history.push(Urls.Home); }); it("should go forward and should go back", async () => { render(); const back = screen.getByText("BACK"); const next = screen.getByText("NEXT"); fireEvent.click(next); expect(history.location.pathname).toBe(Urls.About); fireEvent.click(next); expect(history.location.pathname).toBe(Urls.Start); fireEvent.click(back); expect(history.location.pathname).toBe(Urls.About); }); });
The render
function will call the renderInRouter
higher-order component and supply a component with routing for testing. The single history object that all code references is imported in the same way as the application code:
import { history } from "../../history";
Testing Hooks
The react-hooks-testing-library allows us to test Hooks in isolation with the renderHook
function:
javascript it("should provide forward and backwards navigation", async () => { const { result } = renderHook(() => useStepper(), { wrapper: ({ children }) => ( <> <Router history={history}> <ApplicationNavigator /> {children} </Router> </> ) }); expect(result.current.currentStep).toBe(0); expect(result.current.cantGoBack).toBe(true); expect(result.current.cantProceed).toBe(false); await act(async () => { result.current.nextStepAction(); }); expect(result.current.currentStep).toBe(1); expect(result.current.cantGoBack).toBe(false); expect(result.current.cantProceed).toBe(false); expect(history.location.pathname).toBe(Urls.About); await act(async () => { result.current.previousStepAction(); }); expect(history.location.pathname).toBe(Urls.Home); });
The useStepper
Hook can now be tested without being invoked from a specific component.
The vital thing to note is that a wrapper option is supplied to the renderHook
function. We need to wrap the Hook in a Router
to give the useStepper
Hook the correct context to allow the test to flow.
Conclusion
In my experience, jest.mock
is something to be avoided because it gives a false illusion to tests. With the approach outlined in this article that uses MemoryHistory
, you won’t need to do this. In this article, we explored testing the useNavigate
Hook that was introduced in React Router v6. I hope you enjoyed this article, and feel free to leave a comment if you have any questions. Happy testing!
LogRocket: Full visibility into your production React apps
Debugging React applications can be difficult, especially when users experience issues that are hard to reproduce. If you’re interested in monitoring and tracking Redux state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.

LogRocket combines session replay, product analytics, and error tracking – empowering software teams to create the ideal web and mobile product experience. What does that mean for you?
Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay problems as if they happened in your own browser to quickly understand what went wrong.
No more noisy alerting. Smart error tracking lets you triage and categorize issues, then learns from this. Get notified of impactful user issues, not false positives. Less alerts, way more useful signal.
The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.
Modernize how you debug your React apps — start monitoring for free.
It seems the history prop does not exist on Router in v6
This. I was enthusiastic when I read the article was written in 2022, hoping that it would cover react router v6.