React-router version 5 introduced a new family of Hooks that have simplified the process of making components route-aware.
useHistory does a great job of showcasing React Hooks by separating access to the react-router history object into a cross-cutting concern and replaces the previous cumbersome access via higher-order components or render-props that are now a thing of the past.
javascript import { useHistory } from "react-router-dom"; function HomeButton() { let history = useHistory(); function handleClick() { history.push("/home"); } return ( <button type="button" onClick={handleClick}> Go home </button> ); }
The new Hook makes life much more comfortable, but how do I write tests for this Hook? My initial googling brought back stackoverflow posts like this that advocate for using jest.mock.
javascript jest.mock('react-router-dom', () => ({ ...jest.requireActual('react-router-dom'), useHistory: () => ({ push: jest.fn() }) }));
I try and avoid this approach at all costs since I want my tests to simulate real usage. jest.mock
will blitz an essential component from the simulated reality and I might miss some critical regressions with this approach.
Instead, I am going to lean heavily on react router’s MemoryHistory
.
Sample application
I have created this CodeSandbox:
This includes a simple Hook called useStepper that allows the user to navigate forward and back through several application steps:
Each forward or backward navigation uses the history
object returned from useHistory
to navigate to a new component at a new url:
javascript export const useStepper = () => { const history = useHistory(); const nextStepAction = useCallback(() => { setCurrentStep(index + 1); }, [getCurrentStepIndex]); const previousStepAction = useCallback(() => { setCurrentStep(index - 1); }, [getCurrentStepIndex]); useEffect(() => { const { path } = currentSteps[currentStep]; history.push({ pathname: path, state: { previousPath: history.location.pathname } }); }, [currentStep, history]); // rest };
There now follows a few easy steps to take control of useHistory
without jest.mock
.
Step 1: Centralize the history object
I centralize all access to the history object into a single export from one file located at src/history/index.ts:
javascript 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 such as process.env.NODE_ENV === "test";
out of the application code, but I am making an exception in this case.
Step 2: Create a higher-order component to wrap any component under test in a Router
javascript 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";
As mentioned previously, this import will resolve to the central export that both the application code and test code now reference.
Step 3: Test components with useHistory
Testing components that use useHistory
will now be easy with the two previous steps in place.
The useStepper.test.tsx test references the same history
object as the application code to set up the test scenarios:
javascript 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:
javascript 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
jest.mock
is something to be avoided in my experience and gives a false illusion to tests. With the approach outlined in this article, you won’t need to do this.
Happy testing!
Full visibility into production React apps
Debugging React applications can be difficult, especially when users experience issues that are difficult 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 is like a DVR for web apps, recording literally everything that happens on your React app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app's performance, reporting with metrics like client CPU load, client memory usage, and more.
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.