Skip to content

Commit 8481542

Browse files
authored
chore!: next.js 16 prep: pin 'next' peer dep to ^16 (#933) (#942)
1 parent 04868a1 commit 8481542

12 files changed

Lines changed: 1777 additions & 718 deletions

File tree

README.md

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,133 @@
88
in the form `@databiosphere/findable-ui/lib/<path>`, where `<path>` is the path of the file within the `lib`
99
folder.
1010

11+
## Consumer setup for Next.js 16 (Pages Router, static export)
12+
13+
Consuming apps on Next.js 16 must wire up MUI's Emotion cache helpers in `_app.tsx` and `_document.tsx`. Without this wiring, the browser will report React hydration warnings on components styled by MUI + Emotion (which is most of findable-ui).
14+
15+
### Terminology note
16+
17+
This setup involves a package called `@mui/material-nextjs` and a helper called `documentGetInitialProps`. The naming uses "SSR" / "document" terminology because the package targets Next.js generally, including deployments that do run a runtime HTTP server. **It still applies to static-export deployments.** All the work described below runs at `next build` time, during the same step that generates the static `.html` files. There is no runtime server involved.
18+
19+
When the docs say "extract styles on the server," read it as "extract styles during the static HTML build step."
20+
21+
### Why this is needed
22+
23+
- findable-ui components are styled with MUI + Emotion (CSS-in-JS).
24+
- Emotion generates `css-XXXXXXX` class names by hashing styles in the order they are first encountered during the React render tree walk.
25+
- Next.js builds the static HTML by running the React tree once during `next build`, then the browser runs the same tree again during hydration. For hydration to succeed, both passes must produce the same class names.
26+
- Under Next 16 (Turbopack is the default bundler), the module evaluation order during the build can diverge from the order in the browser, so the same component can be assigned a different hash on each pass. React 19 reports this as a hydration mismatch.
27+
- The fix: capture Emotion's style cache during the build's HTML generation step and inject the resulting `<style>` tags into the static HTML. The browser then hydrates against the exact class names the build wrote.
28+
29+
### Required packages
30+
31+
Install in the consuming project:
32+
33+
```bash
34+
npm install @mui/material-nextjs @emotion/server
35+
```
36+
37+
- `@mui/material-nextjs` — MUI's official integration package; provides the cache provider and the document-head extractor
38+
- `@emotion/server` — peer dependency of the document helper; used during the build to flush Emotion styles into the HTML
39+
40+
### Opt out of Turbopack (required for now)
41+
42+
Next 16 makes Turbopack the default bundler. Turbopack + Pages Router + MUI is currently broken — Emotion class names diverge between the static HTML and the browser hydration pass, producing the exact hydration mismatch this section is meant to fix. The wiring above is still required, but it does not work under Turbopack.
43+
44+
Until the upstream fix lands ([vercel/next.js#82607](https://github.com/vercel/next.js/issues/82607)), pin every `next dev` / `next build` invocation to webpack:
45+
46+
```jsonc
47+
// package.json
48+
"scripts": {
49+
"dev": "next dev --webpack",
50+
"build": "next build --webpack"
51+
}
52+
```
53+
54+
Webpack is still fully supported in Next 16 — Turbopack was promoted to default, not "webpack removed." The deprecation timeline for the webpack fallback hasn't been published, but the `--webpack` flag is a temporary workaround and is expected to be removed in a future major. **Subscribe to [vercel/next.js#82607](https://github.com/vercel/next.js/issues/82607) for status**, and review this pin whenever Next.js publishes a webpack removal notice or the upstream Turbopack + Pages Router + MUI bug is resolved.
55+
56+
This is a Next.js / Turbopack bug, not a findable-ui one. Re-enable Turbopack when [vercel/next.js#82607](https://github.com/vercel/next.js/issues/82607) is closed.
57+
58+
### `_app.tsx`
59+
60+
Wrap the app in `AppCacheProvider`:
61+
62+
```tsx
63+
import { AppCacheProvider } from "@mui/material-nextjs/v16-pagesRouter";
64+
import type { EmotionCache } from "@emotion/react";
65+
import type { AppProps } from "next/app";
66+
import type { JSX } from "react";
67+
68+
type MyAppProps = AppProps & {
69+
emotionCache?: EmotionCache;
70+
};
71+
72+
function MyApp(props: MyAppProps): JSX.Element {
73+
const { Component, emotionCache, pageProps } = props;
74+
return (
75+
<AppCacheProvider emotionCache={emotionCache}>
76+
{/* existing theme providers and layout */}
77+
<Component {...pageProps} />
78+
</AppCacheProvider>
79+
);
80+
}
81+
82+
export default MyApp;
83+
```
84+
85+
### `_document.tsx`
86+
87+
Add `DocumentHeadTags` inside `<Head>` and assign `documentGetInitialProps` as the static `getInitialProps`:
88+
89+
```tsx
90+
import {
91+
documentGetInitialProps,
92+
DocumentHeadTags,
93+
} from "@mui/material-nextjs/v16-pagesRouter";
94+
import type { DocumentHeadTagsProps } from "@mui/material-nextjs/v16-pagesRouter";
95+
import Document, { Head, Html, Main, NextScript } from "next/document";
96+
import type { DocumentContext } from "next/document";
97+
import type { JSX } from "react";
98+
99+
class MyDocument extends Document<DocumentHeadTagsProps> {
100+
render(): JSX.Element {
101+
return (
102+
<Html>
103+
<Head>
104+
<DocumentHeadTags {...this.props} />
105+
{/* other head content */}
106+
</Head>
107+
<body>
108+
<Main />
109+
<NextScript />
110+
</body>
111+
</Html>
112+
);
113+
}
114+
}
115+
116+
MyDocument.getInitialProps = async (ctx: DocumentContext) => {
117+
return await documentGetInitialProps(ctx);
118+
};
119+
120+
export default MyDocument;
121+
```
122+
123+
### Note on `next-mdx-remote@6`
124+
125+
Consumers using `next-mdx-remote` to render MDX content must pass `blockJS: false` to `serialize()`. Version 6 added a `blockJS: true` default that strips all JavaScript expressions from MDX during compilation — including JSX attribute expressions like `<Breadcrumbs breadcrumbs={[...]} />`. With the default, those props are silently dropped at build time and the receiving component renders with no props. Setting `blockJS: false` preserves expression-valued attributes. The narrower `blockDangerousJS: true` setting (default) still blocks `eval` / `new Function` / etc., so the safety net for actually-dangerous patterns is retained.
126+
127+
```ts
128+
import { serialize } from "next-mdx-remote/serialize";
129+
130+
const mdxSource = await serialize(content, {
131+
blockJS: false,
132+
// ...your existing options
133+
});
134+
```
135+
136+
If you use findable-ui's `buildStaticProps` helper (`@databiosphere/findable-ui/lib/utils/mdx/staticGeneration/staticProps`), `blockJS: false` is already the default — no consumer change needed.
137+
11138
## Developing findable-ui alongside a consuming app
12139

13140
Use `scripts/link.sh` to build and install a local copy of findable-ui
@@ -17,21 +144,25 @@ into a consuming project (e.g. ncpi-dataset-catalog, data-browser):
17144
2. Set `node` version to `22.13.0`.
18145
3. Run `npm install` in both repositories.
19146
4. From the consuming project directory:
147+
20148
```bash
21149
../findable-ui/scripts/link.sh
22150
```
151+
23152
This compiles TypeScript, packs the output, installs it into
24153
`node_modules`, clears the Next.js cache, and starts the dev server.
25154

26155
5. To iterate: Ctrl+C the dev server, make changes in findable-ui, and
27156
hit up-arrow to re-run the script.
28157

29158
6. To restore the published version:
159+
30160
```bash
31161
../findable-ui/scripts/unlink.sh
32162
```
33163

34164
Consuming projects can optionally add thin wrappers in their `package.json` scripts:
165+
35166
```json
36167
{
37168
"scripts": {

0 commit comments

Comments
 (0)