msw
msw copied to clipboard
MSW does not mock APIS in react-router-6 loader in the first load
Prerequisites
- [X] I confirm my issue is not in the opened issues
- [X] I confirm the Frequently Asked Questions didn't contain the answer to my issue
Environment check
- [X] I'm using the latest
msw
version - [X] 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
Expected behavior
Mocks should work the first time for loader APIs
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)
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.
@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.
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>
)
})
@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} />);
});
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?
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.
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
Any updates??
@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.
What if we wrap createBrowserRouter
in a function:
export const makeRouter = () => createBrowserRouter([])
<RouterProvider router={makeRouter()} />
@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} />); });
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.
Any news?
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.
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 ? ( <Router /> ) : ( <div className="w-screen h-screen grid text-2xl items-center justify-center">
Dev's life is hard!
Loading mock up apis...
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 using
setupServer 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.
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).
As @marcomuser stated. Here is the solution
function App() {
const router = useMemo(() => {
return createBrowserRouter([
{
path: "/",
// ...
},
])
}, [])
return <RouterProvider router={router}></RouterProvider>
}
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>,
)
})
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:
- 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>
);
- 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):
- Evaluate imports of main.js
- Find App.js import and load it
- Execute the App.js code that is not within functions. createBrowserRouter is invoked which fires off the react-router loaders
- main.js continues to be executed. This starts the worker and then renders the react root
- 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.
In my case exporting the worker.start() from another file worked for me:
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();
}
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"
In my case exporting the worker.start() from another file worked for me:
![]()
This worked perfectly, also it needs to be called as soon as possible and before createBrowserRouter
Any updates?
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;
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.