-
-
Notifications
You must be signed in to change notification settings - Fork 211
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
add next.js app router example #101
base: main
Are you sure you want to change the base?
Changes from all commits
759bf1c
f45cce8
eb017b0
704b320
bb7ea62
c01169b
40ce6d7
29979fc
c745f58
fc467a3
ea6ed2e
a7ef8f7
dc7a437
b3db34c
d2c3e03
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. | ||
|
||
# dependencies | ||
/node_modules | ||
/.pnp | ||
.pnp.js | ||
.yarn/install-state.gz | ||
|
||
# testing | ||
/coverage | ||
|
||
# next.js | ||
/.next/ | ||
/out/ | ||
|
||
# production | ||
/build | ||
|
||
# misc | ||
.DS_Store | ||
*.pem | ||
|
||
# debug | ||
npm-debug.log* | ||
yarn-debug.log* | ||
yarn-error.log* | ||
|
||
# local env files | ||
.env*.local | ||
|
||
# vercel | ||
.vercel | ||
|
||
# typescript | ||
*.tsbuildinfo | ||
next-env.d.ts |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). | ||
|
||
## Getting Started | ||
|
||
First, run the development server: | ||
|
||
```bash | ||
npm run dev | ||
# or | ||
yarn dev | ||
# or | ||
pnpm dev | ||
# or | ||
bun dev | ||
``` | ||
|
||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. | ||
|
||
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. | ||
|
||
This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. | ||
|
||
## Learn More | ||
|
||
To learn more about Next.js, take a look at the following resources: | ||
|
||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. | ||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. | ||
|
||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! | ||
|
||
## Deploy on Vercel | ||
|
||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. | ||
|
||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
import type { Metadata } from 'next' | ||
import { Inter } from 'next/font/google' | ||
import { MSWProvider } from './msw-provider' | ||
|
||
if (process.env.NEXT_RUNTIME === 'nodejs') { | ||
const { server } = require('@/mocks/node') | ||
server.listen() | ||
} | ||
|
||
const inter = Inter({ subsets: ['latin'] }) | ||
|
||
export const metadata: Metadata = { | ||
title: 'Create Next App', | ||
description: 'Generated by create next app', | ||
} | ||
|
||
export default function RootLayout({ | ||
children, | ||
}: Readonly<{ | ||
children: React.ReactNode | ||
}>) { | ||
return ( | ||
<html lang="en"> | ||
<body className={inter.className}> | ||
<MSWProvider>{children}</MSWProvider> | ||
</body> | ||
</html> | ||
) | ||
Comment on lines
+23
to
+28
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I understand that the main goal of the provider is to ensure the service workers are loaded before the components makes any API calls But it's not ideal that we need to change our application behaviour to only use client components. We could update the layout.tsx code to conditionally wrap the children if the application is running in dev, this could still let us use server rendering in production. But even with that, what about using server component specific logic (eg: Using There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @kettanaito It won't force the components within However, I think passing There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @dbk91 You're absolutely right. Can ignore my concerns since they are incorrect There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
That is intended. Your app mustn't render until MSW is activated. If it does, it may fire HTTP requests and nothing will be there to intercept them. You must use |
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
'use client' | ||
|
||
import { useState } from 'react' | ||
|
||
export type Movie = { | ||
id: string | ||
title: string | ||
} | ||
|
||
export function MovieList() { | ||
const [movies, setMovies] = useState<Array<Movie>>([]) | ||
|
||
const fetchMovies = () => { | ||
fetch('/graphql', { | ||
method: 'POST', | ||
headers: { | ||
'Content-Type': 'application/json', | ||
}, | ||
body: JSON.stringify({ | ||
query: ` | ||
query ListMovies { | ||
movies { | ||
id | ||
title | ||
} | ||
} | ||
`, | ||
}), | ||
}) | ||
.then((response) => response.json()) | ||
.then((response) => { | ||
setMovies(response.data.movies) | ||
}) | ||
.catch(() => setMovies([])) | ||
} | ||
|
||
return ( | ||
<div> | ||
<button id="fetch-movies-button" onClick={fetchMovies}> | ||
Fetch movies | ||
</button> | ||
{movies.length > 0 ? ( | ||
<ul id="movies-list"> | ||
{movies.map((movie) => ( | ||
<li key={movie.id}>{movie.title}</li> | ||
))} | ||
</ul> | ||
) : null} | ||
</div> | ||
) | ||
} |
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,44 @@ | ||||||||||||||||||||||
'use client' | ||||||||||||||||||||||
|
||||||||||||||||||||||
import { Suspense, use } from 'react' | ||||||||||||||||||||||
import { handlers } from '@/mocks/handlers' | ||||||||||||||||||||||
|
||||||||||||||||||||||
const mockingEnabledPromise = | ||||||||||||||||||||||
typeof window !== 'undefined' | ||||||||||||||||||||||
? import('@/mocks/browser').then(async ({ worker }) => { | ||||||||||||||||||||||
await worker.start({ | ||||||||||||||||||||||
onUnhandledRequest(request, print) { | ||||||||||||||||||||||
if (request.url.includes('_next')) { | ||||||||||||||||||||||
return | ||||||||||||||||||||||
} | ||||||||||||||||||||||
print.warning() | ||||||||||||||||||||||
}, | ||||||||||||||||||||||
}) | ||||||||||||||||||||||
worker.use(...handlers) | ||||||||||||||||||||||
|
||||||||||||||||||||||
console.log(worker.listHandlers()) | ||||||||||||||||||||||
}) | ||||||||||||||||||||||
: Promise.resolve() | ||||||||||||||||||||||
|
||||||||||||||||||||||
export function MSWProvider({ | ||||||||||||||||||||||
children, | ||||||||||||||||||||||
}: Readonly<{ | ||||||||||||||||||||||
children: React.ReactNode | ||||||||||||||||||||||
}>) { | ||||||||||||||||||||||
Comment on lines
+23
to
+27
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Some success 😄 The reason the interval is cleared in the Svelte example is because this listener for In the Next example, this event listener is not called (presumably I'm not 100% sure how it works, since in Svelte 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 In my eyes it depends upon if it's a bug or not a bug that There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I tried both approaches, named and anonymous export, but both fails on: In browser logs I clearly see that the worker has started.
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:
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 commentThe 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 commentThe 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 commentThe 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.
So you are saying that interval keeps the I don't see There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. looking again, it looks like more generally it's about 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 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 commentThe 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 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. |
||||||||||||||||||||||
// If MSW is enabled, we need to wait for the worker to start, | ||||||||||||||||||||||
// so we wrap the children in a Suspense boundary until it's ready. | ||||||||||||||||||||||
return ( | ||||||||||||||||||||||
<Suspense fallback={null}> | ||||||||||||||||||||||
<MSWProviderWrapper>{children}</MSWProviderWrapper> | ||||||||||||||||||||||
</Suspense> | ||||||||||||||||||||||
) | ||||||||||||||||||||||
} | ||||||||||||||||||||||
|
||||||||||||||||||||||
function MSWProviderWrapper({ | ||||||||||||||||||||||
children, | ||||||||||||||||||||||
}: Readonly<{ | ||||||||||||||||||||||
children: React.ReactNode | ||||||||||||||||||||||
}>) { | ||||||||||||||||||||||
use(mockingEnabledPromise) | ||||||||||||||||||||||
return children | ||||||||||||||||||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
import { MovieList } from './movie-list' | ||
|
||
export type User = { | ||
firstName: string | ||
lastName: string | ||
} | ||
|
||
async function getUser() { | ||
const response = await fetch('https://api.example.com/user') | ||
const user = (await response.json()) as User | ||
return user | ||
} | ||
|
||
export default async function Home() { | ||
const user = await getUser() | ||
|
||
return ( | ||
<main> | ||
<p id="server-side-greeting">Hello, {user.firstName}!</p> | ||
<MovieList /> | ||
</main> | ||
) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
import { setupWorker } from 'msw/browser' | ||
|
||
export const worker = setupWorker() |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
import { http, graphql, HttpResponse } from 'msw' | ||
import type { User } from '@/app/page' | ||
import type { Movie } from '@/app/movie-list' | ||
|
||
export const handlers = [ | ||
http.get<never, never, User>('https://api.example.com/user', () => { | ||
return HttpResponse.json({ | ||
firstName: 'Sarah', | ||
lastName: 'Maverick', | ||
}) | ||
}), | ||
graphql.query<{ movies: Array<Movie> }>('ListMovies', () => { | ||
return HttpResponse.json({ | ||
data: { | ||
movies: [ | ||
{ | ||
id: '6c6dba95-e027-4fe2-acab-e8c155a7f0ff', | ||
title: '123 Lord of The Rings', | ||
}, | ||
{ | ||
id: 'a2ae7712-75a7-47bb-82a9-8ed668e00fe3', | ||
title: 'The Matrix', | ||
}, | ||
{ | ||
id: '916fa462-3903-4656-9e76-3f182b37c56f', | ||
title: 'Star Wars: The Empire Strikes Back', | ||
}, | ||
], | ||
}, | ||
}) | ||
}), | ||
] |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
import { setupServer } from 'msw/node' | ||
import { handlers } from './handlers' | ||
|
||
export const server = setupServer(...handlers) |
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.
is it recommended to do it here, or in the instrumentation file?
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.
@bitttttten, it is recommended to enable it in
layout.tsx
. You can forget about the instrumentation file. I've tried it before, but it doesn't support HMR and isn't a part of your app's life-cycle. That was a bad choice, and the Next.js team members have pointed that out for me.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.
In what way is it suggested to change the mock per test if the suggested way to set up the mocks is via the layout file?
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 wouldnt each url be in a way idempotent? do you mean you want to call 1 url multiple times and see different results?
if you want to run different things depending on search parameters and what not you can do that in the mock
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.
Yes, the same URL with different return values.
Something like a product page.
Product A with stock
Product A out of stock
Product A coming soon
Product A 404 or 500ing.
I suppose I could vary the URL to pick up the mock I want. But in reality, production code would be calling the same URL and getting different return values at different times. Hence my desire to be able to vary the mock per test.
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 think I've sorted it: https://dev.to/votemike/server-side-mocking-for-playwright-in-nextjs-app-router-using-mock-service-worker-2p4i