Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MSW does not mock APIS in react-router-6 loader in the first load #1653

Open
4 tasks done
abhaykumar01234 opened this issue Jul 5, 2023 · 26 comments
Open
4 tasks done
Labels
bug Something isn't working needs:triage Issues that have not been investigated yet. scope:browser Related to MSW running in a browser

Comments

@abhaykumar01234
Copy link

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

@abhaykumar01234 abhaykumar01234 added bug Something isn't working needs:triage Issues that have not been investigated yet. scope:browser Related to MSW running in a browser labels Jul 5, 2023
@abhaykumar01234
Copy link
Author

Same API call gets mocked, if i write it inside useEffect hook in page. Looks like the loader API calls are invoked before the browser.ts file initializes the msw handlers (refer to the network tab screenshot)

@kettanaito
Copy link
Member

Hi, @abhaykumar01234. Thanks for reporting this.

Can you double-check that the app's render root.render() appears after the worker is ready (you await the worker.start promise)? I have a suspicion that's not the case.

Also, I believe this framework you're using is the one Remix is using under the hood, is that correct? I don't have much experience with react-router directly and the loader pattern works fine in the latest Remix with MSW. I suspect, perhaps, that like Svelte, react-router may flush the loaders in a build phase and execute them apart from your application's execution. It would be nice to confirm/deny this.

@abhaykumar01234
Copy link
Author

@kettanaito I have tried to write root.render() inside a worker.start().then(() => {..........));, Also used setTimeout() for root.render(), but it doesn't work here. Although if I introduce a setTimout() inside loader, it works for larger timeout values.
Yes, this way of using loaders is similar to the Remix framework.

If the react-router invokes the loaders before the mounting of page starts and there is no Mock Service worker available in browser, I tried the Server version of MSW as well i.e. server.listen() which we use in tests. But that was also throwing errors.

@wangel13
Copy link

wangel13 commented Jul 6, 2023

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>
  )
})

@abhaykumar01234
Copy link
Author

@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?

@wangel13
Copy link

wangel13 commented Jul 7, 2023

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

I tried with react-query, and it's working in my setup.

@abhaykumar01234
Copy link
Author

abhaykumar01234 commented Jul 9, 2023

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

@abhaykumar01234
Copy link
Author

Any updates??

@kmiwa007
Copy link

@abhaykumar01234 sounds like same scenario as https://twitter.com/rossipedia/status/1611814575401500672?t=FT5BkbsMiff2r3-2mXPRxw&s=19. Looks like you have to find a way to call createBrowserRouter() after prepare() resolves.

@wangel13
Copy link

What if we wrap createBrowserRouter in a function:

export const makeRouter = () => createBrowserRouter([])
<RouterProvider router={makeRouter()} />

@marcomuser
Copy link

@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?

createBrowserRouter itself invokes the loaders, which means it is called before your prepare fn is awaited. Keep in mind that the main purpose of loaders in React-Router/Remix is to decouple data loading from component rendering. Thus, you cannot rely on the component render tree for your setup order. If you want to do something before the loaders are triggered, you need to make sure that createBrowserRouter is executed after the setup you want to run first.

@kettanaito kettanaito moved this to MSW in Roadmap Jul 30, 2023
@kettanaito kettanaito moved this from MSW to Interceptors in Roadmap Jul 30, 2023
@ciceropablo
Copy link

Any news?

@adam-boduch
Copy link

I'm having the same issue. MSW is not working when if make a call in react router loader. It works if I wrap my code in useEffect hook inside the component.

@FredericoGauz
Copy link

FredericoGauz commented Aug 19, 2023

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>
  )
})

I managed to have it working with a similar set up, I also created a simpler wrapper over the router to have something for the development team to see... (I use mobx for state management)

(Sorry the code block is messed up)
`
const startRestApiMocks = async () => {
if (!applicationState.isDevelopment()) return;
const { worker } = await import('./app/mocks/browser.rest.api.mocks');

worker.start();

};

startRestApiMocks().then(() => {
applicationState.setMockApiLoaded(true);
});

const Loader = observer(() => {
return (
<>
{applicationState.mockApiLoaded ? (

) : (




Dev's life is hard!


Loading mock up apis...




)}
</>
);
});
ReactDOM.render(
<React.StrictMode>

</React.StrictMode>,
document.getElementById('root'),
);
`

@manavm1990
Copy link

manavm1990 commented Oct 15, 2023

If you're using Remix, there is a section of the docs📝 showing how to integrate msw..

For 'vanilla' React Router @abhaykumar01234 b/c of the decoupling that React Router does between mounting/rendering and the data loading, it pretty much won't work. I too am facing the same issue. The loaders will 🔥 up and start fetching before MSW can kick in. :(

The browser console will show the 'real fetch' and then after that will come: [MSW] Mocking enabled.

I did get a partial, yet insufficient solution as follows:

In my loader, I import an apiService: import apiService from "../api.service";. Within that, if I put something like:

import worker from "./tests/mocks/worker";

if (process.env.NODE_ENV === "development") {
  worker.start();
}

it works. In my browser, I see: [MSW] Mocking enabled. and it renders the mock 🤡 data.

To be clear, you don't need to use any separate apiService, and your imports might look different. I prefer export default, so mine is: import worker from "./tests/mocks/worker"; (no {s).

But, if you do something like this directly wherever your loaders are, it should start the mocking 🤡 .

insufficient solution

For me, this was still insufficient b/c I have no way of running npm t for my tests ✅ . I get: Invariant Violation: [MSW] Failed to execute setupWorkerin a non-browser environment. Consider usingsetupServer for Node.js environment instead.

That's because despite setting up my tests to use aforementioned server, when it starts loading it's getting 😕 by the 'browser worker' in the loader/API service.

I then tried something like this, which is really sloppy, but since the intercept setup has to happen in the API service (or the loader)...

if (process.env.NODE_ENV === "development") {
  const workerModule = await import("./tests/mocks/worker");
  workerModule.default.start();
} else if (process.env.NODE_ENV === "test") {
  beforeAll(() => server.listen());

  afterEach(() => {
    server.resetHandlers();
  });

  // clean up once the tests are done
  afterAll(() => server.close());
}

It kind of seemed to work intermittently at best...

I could probably restructure the app to do some more decoupling, but not going to bother. Going to stick with Cypress intercept, which pretty much solves the same problem 🤞🏾 and makes it a bit easier than React Testing Library and msw for this case, at least.

@marcomuser
Copy link

As I stated above you can use msw with react-router loaders. The only important thing is that the worker is started before createBrowserRouter is called. For this, you can for example simply memoize createBrowserRouter with useMemo and call it within your App component. This will make sure that worker.start() is called before the loaders are initiated. This will also let it work in typical testing setups (with vitest for example).

@stefan-huck
Copy link

As @marcomuser stated. Here is the solution

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

@aaronschwartz
Copy link

For those that are stuck this is what helped me understand it better.

Does NOT work because createBrowserRouter is called before MSW is initialized

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

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

async function deferRender() {
    if (process.env.NODE_ENV !== 'development') {
        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()
}

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

DOES work because createBrowserRouter is called after MSW is initialized

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

const createRouter = () => createBrowserRouter([...]);

async function deferRender() {
    if (process.env.NODE_ENV !== 'development') {
        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()
}

deferRender().then(() => {
    ReactDOM.createRoot(document.getElementById('root')!).render(
        <React.StrictMode>
            <RouterProvider router={createRouter()}/>
        </React.StrictMode>,
    )
})

@marcomuser
Copy link

marcomuser commented Oct 26, 2023

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: #1653 (comment). 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.

@ionMoraru
Copy link

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

@wataruoguchi
Copy link

Dynamic-loading of the module that depends on react-router-dom solved the case.

Here's my src/main.ts

import React from "react";
import ReactDOM from "react-dom/client";

enableMocking().then(async () => {
  const { Router } = await import("./router.tsx");
  ReactDOM.createRoot(document.getElementById("root")!).render(
    <React.StrictMode>
      <Router />
    </React.StrictMode>,
  );
});

async function enableMocking() {
  if (import.meta.env.MODE !== "development") return;
  const { worker } = await import("./mocks/browser");
  return worker.start();
}

@xJkit
Copy link

xJkit commented Jan 10, 2024

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"

@RodrigoSosa95
Copy link

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

This worked perfectly, also it needs to be called as soon as possible and before createBrowserRouter

@jacksonwen001
Copy link

Any updates?

@SuroZaqaryan
Copy link

routes.js

async function enableMocking() {
  const { worker } = await import('../server/browser');
  return worker.start();
}

await enableMocking();

const router = createBrowserRouter([]);

export default router;

App.js

import React from 'react';
import { RouterProvider } from 'react-router-dom';
import router from 'routes/routes';

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

export default App;

@dargue3
Copy link

dargue3 commented Aug 10, 2024

I've been having these same issues where initial load of an app with react-router deferred loaders was causing the mocked calls to go unhandled. My index.tsx looks the same as what people have here:

if (isMocked()) {
  const { startWorker } = await import('./mocks/browser.js');
  await startWorker();
}

const app = ReactDOM.createRoot(document.getElementById('uhc')!);

app.render(
  <AppProviders>
    <RouterProvider router={createRouter()} />
  </AppProviders>,
);

The trick that just fixed it for me, which I cannot explain, is to ensure you have a leading slash in the path name.

export const mockGetRoute = http.get('/api/myRoute', () => {
  return HttpResponse.json({});
});

For whatever reason, without the leading slash, the routes would work fine after the initial load, but during initial defer() load these would not be called properly. What's odd is that there were some of my Routes that worked perfectly fine with no leading slash, and it seemed like once I was loading ~3 loaders at once for any given route, I was running into this issue where MSW wasn't matching the API calls anymore and just letting them 404.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working needs:triage Issues that have not been investigated yet. scope:browser Related to MSW running in a browser
Projects
Status: Needs help
Development

No branches or pull requests