-
-
Notifications
You must be signed in to change notification settings - Fork 211
add next.js app router example #101
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
base: main
Are you sure you want to change the base?
Conversation
Server-side integrationI got the server-side MSW integration working in Next.js by using the export async function register() {
if (process.env.NEXT_RUNTIME === 'nodejs') {
const { server } = await import('./mocks/node')
server.listen()
}
} This allows MSW to intercept server-side requests Next.js makes. Downsides
|
a17bcd1
to
06e8a00
Compare
* this module and runs it during the build | ||
* in Node.js. This makes "msw/browser" import to fail. | ||
*/ | ||
const { worker } = await import('../mocks/browser') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Next.js puts this dynamic import from the browser runtime to the Node.js build by moving it to the top of the module.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about fixing like this?
if (typeof window !== 'undefined') {
const { worker } = await import('../mocks/browser')
await worker.start()
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This didn't work for me. I had to do this instead:
if (process.env.NEXT_RUNTIME !== "nodejs") {
const { worker } = await import("../mocks/browser");
await worker.start();
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@brycefranzen, that may actually work around webpack resolving this import in Node.js as well. I still think that's a bug though.
examples/with-next/next.config.mjs
Outdated
* export conditions and don't try to import "msw/browser" code | ||
* that's clearly marked as client-side only in the app. | ||
*/ | ||
if (isServer) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a hack. I'm not sure why webpack has trouble resolving export conditions. I suspect this isn't webpack's fault. Next.js runs a pure client-side component in Node.js during SSR build, which results in webpack thinking those client-side imports must be resolved in Node.js.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@kettanaito I think the 'use client'
directive is a bit of a misnomer. Components marked with that directive can still be SSR and are by default in Next unless you lazy load with ssr: false
. Obviously anything in useEffect
would only run on the client, so I'm not sure why the dynamic import you have in the other file is placed in a Node.js runtime. Let me know if I'm missing any context.
Having said that, I pulled this repository down and ran dev and build and both succeeded.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Got it, thanks for clarifying, @dbk91!
I suspect webpack extracts that import and puts it at the top of the module for whichever optimization. This is a bit odd since import()
is a valid JavaScript API in the browser so it can certainly be client-side only.
I know this example succeeds. I've added tests to confirm that and they are passing. But I'm not looking for the first working thing. I'm looking for an integration that'd last and make sense for developers. This one, in its current state, doesn't, as it has a couple of fundamentals problems.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Understood, that makes sense! I totally missed your follow up messages in your original Tweet—I was expecting something non-functional and didn't realize there was extra work to get these tests passing.
Either way, I've been following this for quite some time and appreciate the work you've put into MSW and specifically this integration. My team was using it prior to upgrading to app router and we've sorely missed it, but that's on us for upgrading.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I know it's not a long-term solution, but `await eval("import('msw/node')") is an option to avoid nextjs trying to be clever.
I'm our company we are using this example as a reference. |
is it working? I've tried to use, but it's showing these messages below:
`[MSW] Warning: intercepted a request without a matching request handler: |
Not checked it yet, We just set it up. |
'use client' | ||
import { useEffect, useState } from 'react' | ||
|
||
export function MockProvider({ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about using suspense?
mockProvider.tsx
'use client'
let triggered = false
async function enableApiMocking() {
const { worker } = await import('../mocks/browser')
await worker.start()
}
export function MockProvider() {
if (!triggered) {
triggered = true
throw enableApiMocking()
}
return null
}
layout.tsx
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode
}>) {
return (
<html lang="en">
<body className={inter.className}>
<MockProvider />
{children}
</body>
</html>
)
}
By doing so, we can avoid wrapping children in the mock provider client component.
But I am not sure if this is a good solution.
useEffect | Suspense |
---|---|
![]() |
![]() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The goal of this is to defer the rendering of the children until the service worker is activated. You are proposing keeping the state internally but I don't see it affecting {children}
. So they will render, and if they make any HTTP requests, those will not be intercepted because the worker is not ready yet.
@kettanaito I don't know why but MSW is not intercepting page request. The mock is enabled but does not catch any fetch. |
how does playwright work with this? My test makes the actual api call instead of the mocked call |
@pandeymangg, there should be nothing specific to Playwright here. You enable MSW in your Next.js app, then navigate to it in a Playwright test and perform the actions you need. |
examples/with-next/app/layout.tsx
Outdated
import { MSWProvider } from './msw-provider' | ||
|
||
if (process.env.NEXT_RUNTIME === 'nodejs') { | ||
const { server } = require('../mocks/node') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would this mean the mocks are bundled with the production code?
Does that have any undesirable side-effects?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@votemike, this likely needs tweaking by adding a new env variable like MOCKS_ENABLED
and checking that one too.
export function MSWProvider({ | ||
children, | ||
}: Readonly<{ | ||
children: React.ReactNode | ||
}>) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
export function MSWProvider({ | |
children, | |
}: Readonly<{ | |
children: React.ReactNode | |
}>) { | |
export default ({ | |
children, | |
}: Readonly<{ | |
children: React.ReactNode | |
}>) => { |
Some success 😄
The reason the interval is cleared in the Svelte example is because this listener for beforeunload
is called.
In the Next example, this event listener is not called (presumably beforeunload
doesn’t get called due to how Next.js handles page changes). Therefore, nothing clears the intervals and the workers are not garbage collected.
I'm not 100% sure how it works, since in Svelte beforeunload
is happening but it doesn't seem like window.location.reload
is being called. I did notice this meant that changing the handler used in the server rendered component doesn't cause it to be reloaded.
I believe Next.js doesn't perform a full page reload because the MSWProvider has a parent (layout.tsx) so a Fast Refresh is triggered. In a normal React setup, the changed file is at the root (pre-rendering) and has no parents, so a full page refresh is triggered.
We can take advantage of the fact that Fast Refresh use the casing of exports in a file to check if hot reload is possible. See this docs page https://nextjs.org/docs/messages/fast-refresh-reload
Exporting a component as an anonymous component breaks this, and so it always triggers a full reload. Therefore, we can make the change here to get HMR (via full page reload).
I think given this info, it's worth still thinking about module.hot.dispose()
which does not force a full page reload. This mimics the behaviour of Svelte, including unfortunately that changing the handlers doesn't reflect in the server-rendered components (until manual reload).
In my eyes it depends upon if it's a bug or not a bug that beforeunload
isn't being run in Next.js when there's HMR for the MSWProvider. If that isn't a bug, then there's no reason for setInterval to be cleared and so no reason for the extra worker to disappear. As a relevant note, Svelte mentions something along these lines in their HMR package. But at the moment I don't see a massive amount of difference between using beforeunload
here to do this vs module.hot.dispose()
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tried both approaches, named and anonymous export, but both fails on:
[MSW] Warning: intercepted a request without a matching request handler:
In browser logs I clearly see that the worker has started.
Starting MSW worker
[MSW] Mocking enabled.
On server side the requests are mocked, but on client side all requests are just warning logged.
What I am missing? Using Next 14.2
My provider looks like this:
'use client';
import { Suspense, use } from 'react';
const mockingEnabledPromise =
typeof window !== 'undefined' && process.env.NEXT_RUNTIME !== 'nodejs'
? import('../mocks/browser').then(async ({ worker }) => {
console.log('Starting MSW worker');
await worker.start({
onUnhandledRequest(request, print) {
if (request.url.includes('_next')) {
return;
}
const excludedRoutes = [
'cognito-idp.eu-west-1.amazonaws.com',
'cognito-identity.eu-west-1.amazonaws.com',
'google-analytics.com',
];
const isExcluded = excludedRoutes.some((route) => {
return request?.url?.includes(route);
});
if (isExcluded) {
return;
}
print.warning();
},
});
})
: Promise.resolve();
export default ({
children,
}: Readonly<{
children: React.ReactNode;
}>) => {
return (
<Suspense fallback={null}>
<MSWProviderWrapper>{children}</MSWProviderWrapper>
</Suspense>
);
};
function MSWProviderWrapper({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
use(mockingEnabledPromise);
console.log('MSWProviderWrapper children', children);
return children;
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@funes79 not sure this is relevant to this comment I'm afraid. seems like an issue with how you've set up the handlers.
@kettanaito did you see my comment above?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not sure what issues you are trying to solve here.
The current state of this PR gives you client-side mocking. If something is not mocked, follow the Debugging runbook, the issue is likely elsewhere.
The only thing missing in the current state is a proper HMR support for client-side mocking.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are you responding to funes? My original comment is about client side HMR
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@sebws, yes, that is correct.
Thanks for diving deeper into the HMR issue.
Therefore, nothing clears the intervals and the workers are not garbage collected.
So you are saying that interval keeps the worker
object (not the service worker, mind) persist between HMR? That still sounds a bit odd to me. Do you have proof that clearing that interval indeed solves the issue?
I don't see module.hot.dispose()
as the way forward, to be frank. This is a low-level hackery that an average Next.js user shouldn't be exposed to. I don't want to ask developers to do that. The issue is clearly specific to how Next.js handlers client-side HMR, otherwise we had the same issue in other frameworks. This is just a mention that the proper fix for this one is likely on Next.js' side, and that's why we have vercel/next.js#69098.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
looking again, it looks like more generally it's about beforeunload
being called and what that does, in particular deregistering the actual service worker.
context.events.addListener(window, "beforeunload", () => {
if (worker.state !== "redundant") {
context.workerChannel.send("CLIENT_CLOSED");
}
window.clearInterval(context.keepAliveInterval);
});
it's not just the interval which is keeping the worker
object in memory. we can prove this by logging the creation of the interval, clearing it, and the issue persists.
I may have made the original post at 2am and I don't have a full picture of how msw works so some of this is best guess
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
for context on how I'm looking at this for if you or others are interested, I'm using the "Memory" tab of chrome devtools, and taking heap snapshots at various points. then in the filter, adding SetupWorkerApi
and looking at the retainers.
I found this pretty confusing but key for me has been clicking through when there is a line of code referenced, as well as trying out what happens when you right click -> ignore this retainer.
await page.goto('/', { waitUntil: 'networkidle' }) | ||
|
||
const greeting = page.locator('#server-side-greeting') | ||
await expect(greeting).toHaveText('Hello, John!') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
await expect(greeting).toHaveText('Hello, John!') | |
await expect(greeting).toHaveText('Hello, Sarah!') |
If the test is about checking the mocked data, it should match what's in the handler, right? Same for the other test. :)
After using this example I've found that Next's partial prerendering feature doesn't work properly with the way |
I used this integration and so far it works fine. However I extended if (process.env.NEXT_RUNTIME === 'nodejs') {
import('@/mocks/node').then(async ({ server }) => {
server.listen({
onUnhandledRequest(request, print) {
if (request.url.includes('_next') || request.url === 'http://localhost:8969/stream') {
return;
}
print.warning();
},
});
});
} |
Just a heads up that this approach doesn't work with any requests made within Next.js middleware (probalby we should add disclaimer to PR itself and comment in the example itself?) I think it's probably challenging to make it work due to |
Nevermind, just use JSON server. |
For anyone looking to setup mocking for client side API calls in Next.js.
Of course since the mswjs is loaded dynamically, then the Next.js app is most likely rendered before the service worker is loaded in to the browser... It might or might not be a problem, but would be nice if there would be a standard way how to suspense rendering of the app until service worker is ready to intercept requests |
Waiting for an example... |
I am using NextJS 15.2.4 and am successfully mocking server-side API calls by starting export default async function Layout(props: { children: React.ReactNode }) {
if (
process.env.NEXT_RUNTIME === "nodejs" &&
process.env.MOCKS_ENABLED === "true"
) {
const { server } = await import("@/msw/node");
server.listen({ onUnhandledRequest: "error" });
}
...
} Where import { setupServer } from "msw/node";
import { handlers } from "./handlers";
export const server = setupServer(...handlers); The good news is it works great, the bad news is that making changes to our Next route handlers (that are calling mocked APIs) often - but not always - seems to stop I can't see any pattern to this either. At first I thought maybe uncaught exceptions in our route handlers may be killing I have tried moving the code that starts I am at a loss to debug this but it is happening often enough that I am unsure if we can continue with EDIT: we are using TurboPack if that makes any difference? |
@robcaldecott I have also route handler mocking issue in Next.js |
OK, more info on #101 (comment) After doing some tests, it is definitely TurboPack causing |
@robcaldecott Thanks for sharing that. |
I experienced a similar issue. I could pinpoint it to an Digging deeper I was wondering, why Next.js wants to compile this file, because every time this request happened, it showed For me, the fix was to move this file to the |
Everything works fine on the client, but the path This is the related code I could find in the Next.js codebase: |
Adds a Next.js 14 (App directory ) + MSW usage example.
Todos