diff --git a/packages/next/build/webpack/loaders/next-middleware-ssr-loader/render.ts b/packages/next/build/webpack/loaders/next-middleware-ssr-loader/render.ts index 70a3ae3596ef74e..78bb79ab5903cd2 100644 --- a/packages/next/build/webpack/loaders/next-middleware-ssr-loader/render.ts +++ b/packages/next/build/webpack/loaders/next-middleware-ssr-loader/render.ts @@ -45,7 +45,14 @@ export function getRender({ const renderServerComponentData = isServerComponent ? query.__flight__ !== undefined : false + + const serverComponentProps = + isServerComponent && query.__props__ + ? JSON.parse(query.__props__) + : undefined + delete query.__flight__ + delete query.__props__ const req = { url: pathname, @@ -73,6 +80,7 @@ export function getRender({ supportsDynamicHTML: true, concurrentFeatures: true, renderServerComponentData, + serverComponentProps, serverComponentManifest: isServerComponent ? rscManifest : null, ComponentMod: null, } diff --git a/packages/next/client/index.tsx b/packages/next/client/index.tsx index 85a902d32443f9e..e7ab94e6b57a3eb 100644 --- a/packages/next/client/index.tsx +++ b/packages/next/client/index.tsx @@ -1,6 +1,6 @@ /* global location */ import '@next/polyfill-module' -import React from 'react' +import React, { useState } from 'react' import ReactDOM from 'react-dom' import { StyleRegistry } from 'styled-jsx' import { HeadManagerContext } from '../shared/lib/head-manager-context' @@ -35,6 +35,7 @@ import { RouteAnnouncer } from './route-announcer' import { createRouter, makePublicRouterInstance, useRouter } from './router' import isError from '../lib/is-error' import { trackWebVitalMetric } from './vitals' +import { RefreshContext } from './rsc' /// @@ -643,12 +644,30 @@ const wrapApp = let RSCComponent: (props: any) => JSX.Element if (process.env.__NEXT_RSC) { + const { + createFromFetch, + } = require('next/dist/compiled/react-server-dom-webpack') function createResponseCache() { return new Map() } const rscCache = createResponseCache() + function fetchFlight(href: string, props?: any) { + const url = new URL(href, location.origin) + const searchParams = url.searchParams + searchParams.append('__flight__', '1') + if (props) { + searchParams.append('__props__', JSON.stringify(props)) + } + return fetch(url.toString()) + } + + const getHref = () => { + const { pathname, search } = location + return pathname + search + } + const RSCWrapper = ({ cacheKey, serialized, @@ -658,12 +677,8 @@ if (process.env.__NEXT_RSC) { serialized?: string _fresh?: boolean }) => { - const { - createFromFetch, - } = require('next/dist/compiled/react-server-dom-webpack') let response = rscCache.get(cacheKey) - // If there is no cache, or there is serialized data already if (!response) { response = createFromFetch( serialized @@ -672,16 +687,10 @@ if (process.env.__NEXT_RSC) { t.writable.getWriter().write(new TextEncoder().encode(serialized)) return Promise.resolve({ body: t.readable }) })() - : (() => { - const { search, pathname } = location - const flightReqUrl = - pathname + search + (search ? '&' : '?') + '__flight__' - return fetch(flightReqUrl) - })() + : fetchFlight(getHref()) ) rscCache.set(cacheKey, response) } - const root = response.readRoot() return root } @@ -689,14 +698,31 @@ if (process.env.__NEXT_RSC) { RSCComponent = (props: any) => { const cacheKey = useRouter().asPath const { __flight_serialized__, __flight_fresh__ } = props + const [, dispatch] = useState({}) + // @ts-ignore TODO: remove when react 18 types are supported + const startTransition = React.startTransition + const renrender = () => dispatch({}) + // If there is no cache, or there is serialized data already + function refreshCache(nextProps: any) { + startTransition(() => { + const href = getHref() + const response = createFromFetch(fetchFlight(href, nextProps)) + // FIXME: router.asPath can be different from current location due to navigation + rscCache.set(href, response) + renrender() + }) + } + return ( - - - + + + + + ) } } diff --git a/packages/next/client/rsc.ts b/packages/next/client/rsc.ts new file mode 100644 index 000000000000000..7caa504a4deb315 --- /dev/null +++ b/packages/next/client/rsc.ts @@ -0,0 +1,7 @@ +import { createContext, useContext } from 'react' + +export const RefreshContext = createContext((_: any) => {}) + +export function useRefreshRoot() { + return useContext(RefreshContext) +} diff --git a/packages/next/rsc.d.ts b/packages/next/rsc.d.ts new file mode 100644 index 000000000000000..1e63004efc5fa18 --- /dev/null +++ b/packages/next/rsc.d.ts @@ -0,0 +1 @@ +export * from './dist/client/rsc' diff --git a/packages/next/rsc.js b/packages/next/rsc.js new file mode 100644 index 000000000000000..792373906a60470 --- /dev/null +++ b/packages/next/rsc.js @@ -0,0 +1,3 @@ +module.exports = { + unstable_useRefreshRoot: require('./dist/client/rsc').useRefreshRoot, +} diff --git a/packages/next/server/render.tsx b/packages/next/server/render.tsx index 927e237a72a3a3b..56ae6780be9e312 100644 --- a/packages/next/server/render.tsx +++ b/packages/next/server/render.tsx @@ -209,6 +209,7 @@ export type RenderOptsPartial = { resolvedAsPath?: string serverComponentManifest?: any renderServerComponentData?: boolean + serverComponentProps?: any distDir?: string locale?: string locales?: string[] @@ -356,6 +357,7 @@ export async function renderToHTML( getServerSideProps, serverComponentManifest, renderServerComponentData, + serverComponentProps, isDataReq, params, previewProps, @@ -1004,7 +1006,7 @@ export async function renderToHTML( if (renderServerComponentData) { const stream: ReadableStream = renderToReadableStream( - , + , serverComponentManifest ) const reader = stream.getReader()