-
-
Notifications
You must be signed in to change notification settings - Fork 520
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
Comments
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 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 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. |
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? |
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 |
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 |
What if we wrap
|
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 |
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)
}; startRestApiMocks().then(() => { const Loader = observer(() => { Dev's life is hard!Loading mock up apis... )} </> ); }); ReactDOM.render( <React.StrictMode> </React.StrictMode>, document.getElementById('root'), ); ` |
If you're using Remix, there is a section of the docs📝 showing how to integrate 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: I did get a partial, yet insufficient solution as follows: In my loader, I import an import worker from "./tests/mocks/worker";
if (process.env.NODE_ENV === "development") {
worker.start();
} it works. In my browser, I see: To be clear, you don't need to use any separate But, if you do something like this directly wherever your loaders are, it should start the mocking 🤡 .
For me, this was still insufficient b/c I have no way of running That's because despite setting up my tests to use aforementioned 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 |
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
|
For those that are stuck this is what helped me understand it better. Does NOT work because 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 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:
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>
);
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):
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. |
Dynamic-loading of the module that depends on Here's my 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 That is, put the 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>
);
});
|
Any updates? |
routes.js
App.js
|
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 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 |
Prerequisites
Environment check
msw
versionBrowsers
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
Each page having code
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
The text was updated successfully, but these errors were encountered: