From 60ad1dee76dac03d948977cddee01818a7682d6d Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Wed, 16 Mar 2022 23:04:34 +0100 Subject: [PATCH] Make router able to navigate between rsc pages (#35344) ### Bugfix Made some changes for the data register buffer flushing in #34631 and #34475 that tried to delete the buffer or flush them only once. But turns out it will break the navigation between RSC pages. ### Enhancements Simplify the inline response writer and inline response data for the initial render. Since they're only for the initial render, navigations will leverage the serialized data fetched from router and construct the react tree. ## Bug Fixes #35135 - [x] Related issues linked using `fixes #number` - [x] Integration tests added - [x] Errors have helpful link attached, see `contributing.md` --- packages/next/client/index.tsx | 87 ++++++++----------- .../app/components/nav.server.js | 23 +++++ .../app/pages/index.server.js | 6 +- .../app/pages/next-api/link.server.js | 13 +-- .../app/pages/streaming-rsc.server.js | 14 ++- .../test/rsc.js | 40 +++++++-- 6 files changed, 113 insertions(+), 70 deletions(-) create mode 100644 test/integration/react-streaming-and-server-components/app/components/nav.server.js diff --git a/packages/next/client/index.tsx b/packages/next/client/index.tsx index 59a4b4fa10e8..c00d17f7276c 100644 --- a/packages/next/client/index.tsx +++ b/packages/next/client/index.tsx @@ -678,51 +678,38 @@ if (process.env.__NEXT_RSC) { } = require('next/dist/compiled/react-server-dom-webpack') const encoder = new TextEncoder() - const serverDataBuffer = new Map() - const serverDataWriter = new Map() - const serverDataCacheKey = getCacheKey() + let initialServerDataBuffer: string[] | undefined = undefined + let initialServerDataWriter: WritableStreamDefaultWriter | undefined = + undefined function nextServerDataCallback(seg: [number, string, string]) { - const key = serverDataCacheKey + ',' + seg[1] if (seg[0] === 0) { - serverDataBuffer.set(key, []) + initialServerDataBuffer = [] } else { - const buffer = serverDataBuffer.get(key) - if (!buffer) + if (!initialServerDataBuffer) throw new Error('Unexpected server data: missing bootstrap script.') - const writer = serverDataWriter.get(key) - if (writer) { - writer.write(encoder.encode(seg[2])) + if (initialServerDataWriter) { + initialServerDataWriter.write(encoder.encode(seg[2])) } else { - buffer.push(seg[2]) + initialServerDataBuffer.push(seg[2]) } } } - function nextServerDataRegisterWriter( - key: string, - writer: WritableStreamDefaultWriter - ) { - const buffer = serverDataBuffer.get(key) - if (buffer) { - buffer.forEach((val) => { + function nextServerDataRegisterWriter(writer: WritableStreamDefaultWriter) { + if (initialServerDataBuffer) { + initialServerDataBuffer.forEach((val) => { writer.write(encoder.encode(val)) }) - buffer.length = 0 - // Clean buffer but not deleting the key to mark bootstrap as complete. - // Then `nextServerDataCallback` will be safely skipped in the future renders. - serverDataBuffer.set(key, []) } - serverDataWriter.set(key, writer) + initialServerDataWriter = writer } // When `DOMContentLoaded`, we can close all pending writers to finish hydration. document.addEventListener( 'DOMContentLoaded', function () { - serverDataWriter.forEach((writer) => { - if (!writer.closed) { - writer.close() - } - }) + if (initialServerDataWriter && !initialServerDataWriter.closed) { + initialServerDataWriter.close() + } }, false ) @@ -748,27 +735,26 @@ if (process.env.__NEXT_RSC) { } function useServerResponse(cacheKey: string, serialized?: string) { - const id = (React as any).useId() - let response = rscCache.get(cacheKey) if (response) return response - const bufferCacheKey = cacheKey + ',' + router.route + ',' + id - if (serverDataBuffer.has(bufferCacheKey)) { + if (initialServerDataBuffer) { const t = new TransformStream() const writer = t.writable.getWriter() response = createFromFetch(Promise.resolve({ body: t.readable })) - nextServerDataRegisterWriter(bufferCacheKey, writer) + nextServerDataRegisterWriter(writer) } else { - response = createFromFetch( - serialized - ? (() => { - const t = new TransformStream() - t.writable.getWriter().write(new TextEncoder().encode(serialized)) - return Promise.resolve({ body: t.readable }) - })() - : fetchFlight(getCacheKey()) - ) + const fetchPromise = serialized + ? (() => { + const t = new TransformStream() + const writer = t.writable.getWriter() + writer.ready.then(() => { + writer.write(new TextEncoder().encode(serialized)) + }) + return Promise.resolve({ body: t.readable }) + })() + : fetchFlight(getCacheKey()) + response = createFromFetch(fetchPromise) } rscCache.set(cacheKey, response) @@ -778,15 +764,16 @@ if (process.env.__NEXT_RSC) { const ServerRoot = ({ cacheKey, serialized, - _fresh, }: { cacheKey: string serialized?: string - _fresh?: boolean }) => { React.useEffect(() => { rscCache.delete(cacheKey) }) + React.useEffect(() => { + initialServerDataBuffer = undefined + }, []) const response = useServerResponse(cacheKey, serialized) const root = response.readRoot() return root @@ -794,10 +781,10 @@ if (process.env.__NEXT_RSC) { RSCComponent = (props: any) => { const cacheKey = getCacheKey() - const { __flight_serialized__, __flight_fresh__ } = props + const { __flight_serialized__ } = props const [, dispatch] = useState({}) const startTransition = (React as any).startTransition - const renrender = () => dispatch({}) + const rerender = () => dispatch({}) // If there is no cache, or there is serialized data already function refreshCache(nextProps: any) { startTransition(() => { @@ -807,17 +794,13 @@ if (process.env.__NEXT_RSC) { ) rscCache.set(currentCacheKey, response) - renrender() + rerender() }) } return ( - + ) } diff --git a/test/integration/react-streaming-and-server-components/app/components/nav.server.js b/test/integration/react-streaming-and-server-components/app/components/nav.server.js new file mode 100644 index 000000000000..23fb83d933a3 --- /dev/null +++ b/test/integration/react-streaming-and-server-components/app/components/nav.server.js @@ -0,0 +1,23 @@ +import Link from 'next/link' + +export default function Nav() { + return ( + <> +
+ + next link + +
+
+ + streaming rsc + +
+
+ + home + +
+ + ) +} diff --git a/test/integration/react-streaming-and-server-components/app/pages/index.server.js b/test/integration/react-streaming-and-server-components/app/pages/index.server.js index 929f47615a2b..4bbf05c55236 100644 --- a/test/integration/react-streaming-and-server-components/app/pages/index.server.js +++ b/test/integration/react-streaming-and-server-components/app/pages/index.server.js @@ -1,5 +1,5 @@ import Foo from '../components/foo.client' -import Link from 'next/link' +import Nav from '../components/nav.server' const envVar = process.env.ENV_VAR_TEST const headerKey = 'x-next-test-client' @@ -14,9 +14,7 @@ export default function Index({ header, router }) {
- - refresh - +