ReactTDD.com

Refactoring to React Router

How do you introduce React Router into a React codebase?

This has been a difficult post to write, for a couple of reasons.

One is that this is a very niche position to be in. Most React projects will already start with a router in place. Maybe they’re server-side rendered (SSR) applications that have the concept of page routes built-in. Or maybe they started out as a single-page application (SPA) that has some pre-defined routes already baked in, right from the beginning.

So the only time this refactor is necessary is when you’ve started an app inside-out, growing it organically from something small into something much larger.

The second (and more important) reason that this has been a difficult post is because I’ve been figuring out what is the essential idea to this type of refactor. And actually, I think it’s pretty small.

It turns out there’s only two things you need to know.

1. Introducing React Router (or any router) is probably not a refactor in the traditional sense.

A refactor–in the traditional sense–would mean you don’t make any externally visible changes. But if you’re introducing a router you are making an externally visible change. You are creating new entrypoints into your application. If I can point my browser at http://example.com/mySpa/customer/123 then that is a new entrypoint into your application. This is a change in behavior.

2. If you have an existing test suite, you have a choice: add to your existing tests, or split your existing tests.

Imagine you have this test:

it("displays the CustomerForm when button is clicked", async () => {
  render(<App />);
  click(addCustomerButton());

  expect(element("#CustomerForm")).not.toBeNull();
});

Now let’s say you’re introducing React Router with a new route at /addCustomer. Clicking the button still does the same thing, but it makes an observable change to the URL in the user’s browser window.

With test-driven development, if you’re adding behavior, you need a test. And that test should be made to be pass with the simplest possible change. So the problem becomes: can we find a single test that will force us to use bring in React Router?

How about this next test? This is taken from Mastering React Test-Driven Development. (Here’s the definition of renderWithRouter, if you’re curious).

it("renders CustomerForm at the /addCustomer endpoint", () => {
   renderWithRouter(<App />, {
     location: "/addCustomer",
   });
   expect(element("#CustomerForm")).not.toBeNull();
});

I can’t claim to be happy with this test: it’s written in terms of React Router itself. The test is making an assumption that you’re using React Router.

(Maybe if I was being pedantic about this, I’d set the location the JSDOM and then render the App component as normal in the test. This would force the inclusion of the router inside of App.)

So that does the bit about adding and testing the new behavior. But now notice that you’re overtesting because you have two tests with the same expectation:

expect(element("#CustomerForm")).not.toBeNull();

If that bothered you, you could choose to split the first test up:

export const linkFor = (href) =>
  elements("a").find(
    (el) => el.getAttribute("href") === href
  );

it("renders a link to the /addCustomer route", async () => {
  renderWithRouter(<App />);
  expect(linkFor("/addCustomer")).toBeDefined();
});

it("captions the /addCustomer link as 'Add customer'", async () => {
  renderWithRouter(<App />);
  expect(linkFor("/addCustomer")).toContainText(
    "Add customer"
  );
});

In all honesty, this is all you need to know about “refactoring” to React Router.

In summary…

  1. Find the tests that cause navigation changes in their pre-router state.
  2. Decide on the new URLs you want to add.
  3. Treating these URLs as new behavior, add tests of the form “renders MyComponent at the /myComponent endpoint”. These tests should live beside the original navigation tests you found in step 1.
  4. Should you wish to avoid overtesting, you can split apart the original tests, replacing the expectations with new checks that the browser navigates to the correct URL.

— Written by Daniel Irvine on September 29, 2022.