Skip to content

Commit d5b6f3c

Browse files
committed
customized flush effects
change cond inject initial styles to end of head, add test revert pnpm lock
1 parent 213c42f commit d5b6f3c

File tree

16 files changed

+185
-54
lines changed

16 files changed

+185
-54
lines changed

packages/next/build/webpack-config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -974,7 +974,7 @@ export default async function getBaseWebpackConfig(
974974
}
975975

976976
const rscSharedRegex =
977-
/(node_modules\/react\/|\/shared\/lib\/(head-manager-context|router-context)\.js|node_modules\/styled-jsx\/)/
977+
/(node_modules\/react\/|\/shared\/lib\/(head-manager-context|router-context|flush-effects)\.js|node_modules\/styled-jsx\/)/
978978

979979
let webpackConfig: webpack.Configuration = {
980980
parallelism: Number(process.env.NEXT_WEBPACK_PARALLELISM) || undefined,

packages/next/client/components/hooks-client-context.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { createContext } from 'react'
2-
import { NextParsedUrlQuery } from '../../server/request-meta'
2+
import type { NextParsedUrlQuery } from '../../server/request-meta'
33

44
export const SearchParamsContext = createContext<NextParsedUrlQuery>(
55
null as any

packages/next/client/components/hooks-client.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@ import {
1212
LayoutRouterContext,
1313
} from '../../shared/lib/app-router-context'
1414

15+
export {
16+
FlushEffectsContext,
17+
useFlushEffects,
18+
} from '../../shared/lib/flush-effects'
19+
1520
/**
1621
* Get the current search params. For example useSearchParams() would return {"foo": "bar"} when ?foo=bar
1722
*/

packages/next/client/components/match-segments.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Segment } from '../../server/app-render'
1+
import type { Segment } from '../../server/app-render'
22

33
export const matchSegment = (
44
existingSegment: Segment,

packages/next/server/app-render.tsx

Lines changed: 30 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import React from 'react'
66
import { ParsedUrlQuery, stringify as stringifyQuery } from 'querystring'
77
import { createFromReadableStream } from 'next/dist/compiled/react-server-dom-webpack'
88
import { renderToReadableStream } from 'next/dist/compiled/react-server-dom-webpack/writer.browser.server'
9-
import { StyleRegistry, createStyleRegistry } from 'styled-jsx'
109
import { NextParsedUrlQuery } from './request-meta'
1110
import RenderResult from './render-result'
1211
import {
@@ -23,6 +22,7 @@ import { htmlEscapeJsonString } from './htmlescape'
2322
import { shouldUseReactRoot, stripInternalQueries } from './utils'
2423
import { NextApiRequestCookies } from './api-utils'
2524
import { matchSegment } from '../client/components/match-segments'
25+
import { FlushEffectsContext } from '../client/components/hooks-client'
2626

2727
// this needs to be required lazily so that `next-server` can set
2828
// the env before we require
@@ -985,18 +985,26 @@ export async function renderToHTMLOrFlight(
985985
}
986986
)
987987

988-
/**
989-
* Style registry for styled-jsx
990-
*/
991-
const jsxStyleRegistry = createStyleRegistry()
988+
let flushEffectsHandler: (() => React.ReactNode) | null = null
989+
function FlushEffects({ children }: { children: JSX.Element }) {
990+
// Reset flushEffectsHandler on each render
991+
flushEffectsHandler = null
992+
const setFlushEffectsHandler = React.useCallback(
993+
(handler: () => React.ReactNode) => {
994+
if (flushEffectsHandler)
995+
throw new Error(
996+
'The `useFlushEffects` hook cannot be used more than once.'
997+
)
998+
flushEffectsHandler = handler
999+
},
1000+
[]
1001+
)
9921002

993-
/**
994-
* styled-jsx styles as React Component
995-
*/
996-
const styledJsxFlushEffect = (): React.ReactNode => {
997-
const styles = jsxStyleRegistry.styles()
998-
jsxStyleRegistry.flush()
999-
return <>{styles}</>
1003+
return (
1004+
<FlushEffectsContext.Provider value={setFlushEffectsHandler}>
1005+
{children}
1006+
</FlushEffectsContext.Provider>
1007+
)
10001008
}
10011009

10021010
/**
@@ -1015,11 +1023,18 @@ export async function renderToHTMLOrFlight(
10151023
const generateStaticHTML = supportsDynamicHTML !== true
10161024
const bodyResult = async () => {
10171025
const content = (
1018-
<StyleRegistry registry={jsxStyleRegistry}>
1026+
<FlushEffects>
10191027
<ServerComponentsRenderer />
1020-
</StyleRegistry>
1028+
</FlushEffects>
10211029
)
10221030

1031+
const flushEffectHandler = (): string => {
1032+
const flushed = ReactDOMServer.renderToString(
1033+
<>{flushEffectsHandler && flushEffectsHandler()}</>
1034+
)
1035+
return flushed
1036+
}
1037+
10231038
const renderStream = await renderToInitialStream({
10241039
ReactDOMServer,
10251040
element: content,
@@ -1031,17 +1046,13 @@ export async function renderToHTMLOrFlight(
10311046
},
10321047
})
10331048

1034-
const flushEffectHandler = (): string => {
1035-
const flushed = ReactDOMServer.renderToString(styledJsxFlushEffect())
1036-
return flushed
1037-
}
1038-
10391049
const hasConcurrentFeatures = !!runtime
10401050

10411051
return await continueFromInitialStream(renderStream, {
10421052
dataStream: serverComponentsInlinedTransformStream?.readable,
10431053
generateStaticHTML: generateStaticHTML || !hasConcurrentFeatures,
10441054
flushEffectHandler,
1055+
flushEffectsToHead: true,
10451056
initialStylesheets,
10461057
})
10471058
}

packages/next/server/node-web-streams-helper.ts

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ export function createFlushEffectStream(
137137
}
138138

139139
export function createHeadInjectionTransformStream(
140-
inject: string
140+
inject: () => string
141141
): TransformStream<Uint8Array, Uint8Array> {
142142
let injected = false
143143
return new TransformStream({
@@ -147,7 +147,7 @@ export function createHeadInjectionTransformStream(
147147
if (!injected && (index = content.indexOf('</head')) !== -1) {
148148
injected = true
149149
const injectedContent =
150-
content.slice(0, index) + inject + content.slice(index)
150+
content.slice(0, index) + inject() + content.slice(index)
151151
controller.enqueue(encodeText(injectedContent))
152152
} else {
153153
controller.enqueue(chunk)
@@ -175,12 +175,14 @@ export async function continueFromInitialStream(
175175
dataStream,
176176
generateStaticHTML,
177177
flushEffectHandler,
178+
flushEffectsToHead,
178179
initialStylesheets,
179180
}: {
180181
suffix?: string
181182
dataStream?: ReadableStream<Uint8Array>
182183
generateStaticHTML: boolean
183184
flushEffectHandler?: () => string
185+
flushEffectsToHead: boolean
184186
initialStylesheets?: string[]
185187
}
186188
): Promise<ReadableStream<Uint8Array>> {
@@ -193,15 +195,22 @@ export async function continueFromInitialStream(
193195

194196
const transforms: Array<TransformStream<Uint8Array, Uint8Array>> = [
195197
createBufferedTransformStream(),
196-
flushEffectHandler ? createFlushEffectStream(flushEffectHandler) : null,
198+
flushEffectHandler && !flushEffectsToHead
199+
? createFlushEffectStream(flushEffectHandler)
200+
: null,
197201
suffixUnclosed != null ? createDeferredSuffixStream(suffixUnclosed) : null,
198202
dataStream ? createInlineDataStream(dataStream) : null,
199203
suffixUnclosed != null ? createSuffixStream(closeTag) : null,
200-
createHeadInjectionTransformStream(
201-
(initialStylesheets || [])
204+
createHeadInjectionTransformStream(() => {
205+
const inlineStyleLinks = (initialStylesheets || [])
202206
.map((href) => `<link rel="stylesheet" href="/_next/${href}">`)
203207
.join('')
204-
),
208+
// TODO-APP: Inject flush effects to end of head in app layout rendering, to avoid
209+
// hydration errors. Remove this once it's ready to be handled by react itself.
210+
const flushEffectsContent =
211+
flushEffectHandler && flushEffectsToHead ? flushEffectHandler() : ''
212+
return inlineStyleLinks + flushEffectsContent
213+
}),
205214
].filter(nonNullable)
206215

207216
return transforms.reduce(

packages/next/server/render.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1269,6 +1269,7 @@ export async function renderToHTML(
12691269
dataStream: serverComponentsInlinedTransformStream?.readable,
12701270
generateStaticHTML,
12711271
flushEffectHandler,
1272+
flushEffectsToHead: false,
12721273
})
12731274
}
12741275

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import React, { createContext, useContext } from 'react'
2+
3+
export type FlushEffectsHook = (callbacks: () => React.ReactNode) => void
4+
5+
export const FlushEffectsContext = createContext<FlushEffectsHook | null>(
6+
null as any
7+
)
8+
9+
export function useFlushEffects(callbacks: () => React.ReactNode): void {
10+
const flushEffectsImpl = useContext(FlushEffectsContext)
11+
// Should have no effects on client where there's no flush effects provider
12+
if (!flushEffectsImpl) return
13+
return flushEffectsImpl(callbacks)
14+
}

test/e2e/app-dir/rsc-basic.test.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ describe('app dir - react server components', () => {
4141
'next.config.js': new FileRef(path.join(appDir, 'next.config.js')),
4242
},
4343
dependencies: {
44+
'styled-jsx': 'latest',
45+
'styled-components': '6.0.0-alpha.5',
4446
react: 'experimental',
4547
'react-dom': 'experimental',
4648
},
@@ -320,11 +322,15 @@ describe('app dir - react server components', () => {
320322
expect(content).toContain('bar.server.js:')
321323
})
322324

323-
it.skip('should SSR styled-jsx correctly', async () => {
324-
const html = await renderViaHTTP(next.url, '/styled-jsx')
325-
const styledJsxClass = getNodeBySelector(html, 'h1').attr('class')
325+
it('should render initial styles of css-in-js in SSR correctly', async () => {
326+
const html = await renderViaHTTP(next.url, '/css-in-js')
327+
const head = getNodeBySelector(html, 'head').html()
326328

327-
expect(html).toContain(`h1.${styledJsxClass}{color:red}`)
329+
// from styled-jsx
330+
expect(head).toMatch(/{color:(\s*)purple;?}/)
331+
332+
// from styled-components
333+
expect(head).toMatch(/{color:(\s*)blue;?}/)
328334
})
329335

330336
it('should support streaming for flight response', async () => {
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import Comp from './styled-jsx.client'
2+
import StyledComp from './styled-components.client'
3+
4+
export default function Page() {
5+
return (
6+
<div>
7+
<Comp />
8+
<StyledComp />
9+
</div>
10+
)
11+
}

0 commit comments

Comments
 (0)