msw: MSW does not mock APIS in react-router-6 loader in the first load

Prerequisites

Environment check

  • I’m using the latest msw version
  • I’m using Node.js version 14 or higher

Browsers

Chromium (Chrome, Brave, etc.)

Reproduction repository

https://github.com/abhaykumar01234/hacker-news

Reproduction steps

npm run dev:mock

Current behavior

I am running a vite application, using react-router-dom:v6 and msw:latest.

I have 2 pages

import { createBrowserRouter, RouterProvider } from "react-router-dom";

const router = createBrowserRouter([
  {
    path: "/",
    element: <App />,
    loader: appLoader,
  },
  {
    path: "/about",
    element: <About />,
    loader: aboutLoader,
  },
]);

const root = ReactDOM.createRoot(
  document.getElementById("root") as HTMLElement
);

if (import.meta.env.VITE_MSW_MOCKED === "1") {
  const { worker } = await import("~/mocks/browser");
  await worker.start({ onUnhandledRequest: "bypass" });
}

root.render(<RouterProvider router={router} />);

Each page having code

import { Link, json, useLoaderData } from "react-router-dom";

export const loader = async () => {
  try {
    const res = await fetch(`${import.meta.env.VITE_BASE_URL}/global`);
    return json(await res.json());
  } catch (err) {
    console.error(err);
    return null;
  }
};

export default function Layout() {
  const data = useLoaderData();

  console.log("home", data);
  return (
    <div>
      <h1>Home</h1>
      <Link to="/about">About</Link>
    </div>
  );
}

and one link to the other page.

When the page loads for the first time, Mocks are enabled but the API endpoint fails. When the links are clicked to navigate back and forth the pages, the mock works the next time

image image

Expected behavior

Mocks should work the first time for loader APIs

About this issue

  • Original URL
  • State: open
  • Created a year ago
  • Reactions: 6
  • Comments: 23 (1 by maintainers)

Most upvoted comments

If you can use ES2022 (with top-level await) you can simply do the following:

import { createRoot } from 'react-dom/client';
import { StrictMode } from 'react';
import { RouterProvider, createBrowserRouter } from "react-router-dom";

if (process.env.NODE_ENV === 'development') {
  const { worker } = await import('./app/testing/mocks/browser.js');
  await worker.start();
}

const router = createBrowserRouter([...]);

const root = createRoot(document.getElementById('root'));

root.render(
  <StrictMode>
    <RouterProvider router={router}/>
  </StrictMode>
);

This would be the easiest setup. If you, however, do not include the msw- and the react-router related code in the same module (file), you need to be a bit more careful due to the nature of how ESM imports work. Let’s assume you have the following two files:

  1. main.js
import { createRoot } from 'react-dom/client';
import { StrictMode } from 'react';

import { App } from './app/App.jsx';

if (process.env.NODE_ENV === 'development') {
  const { worker } = await import('./app/testing/mocks/browser.js');
  await worker.start();
}

const root = createRoot(document.getElementById('root'));

root.render(
  <StrictMode>
    <App />
  </StrictMode>
);
  1. App.jsx
import { RouterProvider, createBrowserRouter } from "react-router-dom";

const router = createBrowserRouter([...]);

export function App() {
  return(
    <RouterProvider router={router}/>
  );
}

Now we need to know one thing about the execution order of this code. The ESM imports are evaluated first! This means the order of execution is the following (roughly speaking):

  1. Evaluate imports of main.js
  2. Find App.js import and load it
  3. Execute the App.js code that is not within functions. createBrowserRouter is invoked which fires off the react-router loaders
  4. main.js continues to be executed. This starts the worker and then renders the react root
  5. Now we are at the point where the App component is rendered

This is why you need to call createBrowserRouter from within the App component (make sure to memoize it!) if you want to follow this file structure. For this you can do it for example as @lucider5 has suggested above: https://github.com/mswjs/msw/issues/1653#issuecomment-1776867147. Hope this helps! I think this could be better documented in the react-router documentation but I don’t see any issues on the msw side. I think we can close this issue.

As @marcomuser stated. Here is the solution

function App() {
  const router = useMemo(() => {
    return createBrowserRouter([
      {
        path: "/",
        // ...
      },
    ])
  }, [])
  return <RouterProvider router={router}></RouterProvider>
}

This works for me:

// Setup MSW
async function prepare() {
  if (import.meta.env.VITE_MSW === 'enabled') {
    const { worker } = await import('./mocks/browser')
    worker.start()
  }

  return Promise.resolve()
}

prepare().then(() => {
  ReactDOM.createRoot(document.getElementById('root')!).render(
    <React.StrictMode>
      <Providers>
        <App />
      </Providers>
    </React.StrictMode>
  )
})

Dynamic import is not necessary. Try this way, and it works.

Just make sure that you create the browser router after resolving the worker.start().

That is, put the createBrowserRouter after the async function enableMocking() like so:

async function enableMocking() {
  if (import.meta.env.VITE_MOCK !== 'TRUE') {
    return;
  }

  const { worker } = await import('./mocks/browser');

  // `worker.start()` returns a Promise that resolves
  // once the Service Worker is up and ready to intercept requests.
  return worker.start();
}

enableMocking().then(() => {
  const router = createBrowserRouter([
    {
      path: '/',
      element: <App />,
      loader: async () => {
        const data = await fetch('/api/test', {
          headers: {
            'Content-Type': 'application/json',
          },
        });
        return data;
      },
      errorElement: <Error />,
    },
  ]);

  ReactDOM.createRoot(document.getElementById('root')).render(
    <React.StrictMode>
        <RouterProvider router={router} />
    </React.StrictMode>
  );
});

  • “react-router-dom”: “^6.21.1”,
  • “msw”: “^2.0.13”

React query hits the API after the mounting of the page and uses useEffect underneath. Correct me if I am wrong. Also, I am looking for a solution to work with plain react-router-dom. loader calls. I know I may sound vanilla here, not using any packages, but shouldn’t it work like that? @wangel13

@wangel13 Doesn’t work for me

import { createBrowserRouter, RouterProvider } from "react-router-dom";

const router = createBrowserRouter([
  {
    path: "/",
    element: <App />,
    loader: appLoader,
    children: [
      {
        path: "/",
        element: <Home />,
        loader: homeLoader,
      },
      {
        path: "/about",
        element: <About />,
        loader: aboutLoader,
      },
    ],
  },
]);

// Setup MSW
async function prepare() {
  if (import.meta.env.VITE_MSW_MOCKED === "1") {
    const { worker } = await import("./mocks/browser");
    await worker.start({ onUnhandledRequest: "bypass" });
  }

  return Promise.resolve();
}

prepare().then(() => {
  const root = ReactDOM.createRoot(
    document.getElementById("root") as HTMLElement
  );
  root.render(<RouterProvider router={router} />);
});
image

For some reason, react-router-6 loaders are invoked before the handlers are mocked in msw.

Did you try it with loaders in your react page making API calls?

In my case exporting the worker.start() from another file worked for me: image image

Any news?

Any updates??