Paul Cowan Contract software developer.

Testing the React Router useNavigate Hook with react-testing-library

3 min read 953

React Router useNavigate

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

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:

Usestepper Hook Navig

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 — .

Paul Cowan Contract software developer.

2 Replies to “Testing the React Router useNavigate Hook with react-testing-library”

    1. This. I was enthusiastic when I read the article was written in 2022, hoping that it would cover react router v6.

Leave a Reply