From 6098ca0eabfdaf4adb1aa16807706dac40a2591d Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Mon, 1 May 2023 18:53:45 -0400 Subject: [PATCH 1/4] Encode $$FORM_ACTION on Server References so that they can be submitted before hydration --- .../react-client/src/ReactFlightClient.js | 12 +- .../src/ReactFlightReplyClient.js | 51 ++++- .../src/ReactFlightServerReferenceRegistry.js | 32 --- .../src/__tests__/ReactFlightDOMForm-test.js | 206 ++++++++++++++++++ scripts/error-codes/codes.json | 3 +- 5 files changed, 262 insertions(+), 42 deletions(-) delete mode 100644 packages/react-client/src/ReactFlightServerReferenceRegistry.js create mode 100644 packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMForm-test.js diff --git a/packages/react-client/src/ReactFlightClient.js b/packages/react-client/src/ReactFlightClient.js index e8034d257e790..4fdbc0d5cea8a 100644 --- a/packages/react-client/src/ReactFlightClient.js +++ b/packages/react-client/src/ReactFlightClient.js @@ -20,6 +20,8 @@ import type { import type {HintModel} from 'react-server/src/ReactFlightServerConfig'; +import type {CallServerCallback} from './ReactFlightReplyClient'; + import { resolveClientReference, preloadModule, @@ -28,13 +30,16 @@ import { dispatchHint, } from './ReactFlightClientConfig'; -import {knownServerReferences} from './ReactFlightServerReferenceRegistry'; +import { + encodeFormAction, + knownServerReferences, +} from './ReactFlightReplyClient'; import {REACT_LAZY_TYPE, REACT_ELEMENT_TYPE} from 'shared/ReactSymbols'; import {getOrCreateServerContext} from 'shared/ReactServerContextRegistry'; -export type CallServerCallback = (id: any, args: A) => Promise; +export type {CallServerCallback}; export type JSONValue = | number @@ -500,6 +505,9 @@ function createServerReferenceProxy, T>( return callServer(metaData.id, bound.concat(args)); }); }; + // Expose encoder for use by SSR. + // TODO: Only expose this in SSR builds and not the browser client. + proxy.$$FORM_ACTION = encodeFormAction; knownServerReferences.set(proxy, metaData); return proxy; } diff --git a/packages/react-client/src/ReactFlightReplyClient.js b/packages/react-client/src/ReactFlightReplyClient.js index 224af305d64b2..412c176e19e11 100644 --- a/packages/react-client/src/ReactFlightReplyClient.js +++ b/packages/react-client/src/ReactFlightReplyClient.js @@ -7,12 +7,7 @@ * @flow */ -import type {Thenable} from 'shared/ReactTypes'; - -import { - knownServerReferences, - createServerReference, -} from './ReactFlightServerReferenceRegistry'; +import type {Thenable, ReactCustomFormAction} from 'shared/ReactTypes'; import { REACT_ELEMENT_TYPE, @@ -39,6 +34,15 @@ type ReactJSONValue = export opaque type ServerReference = T; +export type CallServerCallback = (id: any, args: A) => Promise; + +export type ServerReferenceId = any; + +export const knownServerReferences: WeakMap< + Function, + {id: ServerReferenceId, bound: null | Thenable>}, +> = new WeakMap(); + // Serializable values export type ReactServerValue = // References are passed by their value @@ -363,4 +367,37 @@ export function processReply( } } -export {createServerReference}; +export function encodeFormAction( + this: any => Promise, + identifierPrefix: string, +): ReactCustomFormAction { + const reference = knownServerReferences.get(this); + if (!reference) { + throw new Error( + 'Tried to encode a Server Action from a different instance than the encoder is from. ' + + 'This is a bug in React.', + ); + } + return { + name: '$ACTION_' + reference.id, + method: 'POST', + encType: 'multipart/form-data', + data: null, + }; +} + +export function createServerReference, T>( + id: ServerReferenceId, + callServer: CallServerCallback, +): (...A) => Promise { + const proxy = function (): Promise { + // $FlowFixMe[method-unbinding] + const args = Array.prototype.slice.call(arguments); + return callServer(id, args); + }; + // Expose encoder for use by SSR. + // TODO: Only expose this in SSR builds and not the browser client. + proxy.$$FORM_ACTION = encodeFormAction; + knownServerReferences.set(proxy, {id: id, bound: null}); + return proxy; +} diff --git a/packages/react-client/src/ReactFlightServerReferenceRegistry.js b/packages/react-client/src/ReactFlightServerReferenceRegistry.js deleted file mode 100644 index 06ad06e9b3e46..0000000000000 --- a/packages/react-client/src/ReactFlightServerReferenceRegistry.js +++ /dev/null @@ -1,32 +0,0 @@ -/** - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @flow - */ - -import type {Thenable} from 'shared/ReactTypes'; - -export type CallServerCallback = (id: any, args: A) => Promise; - -type ServerReferenceId = any; - -export const knownServerReferences: WeakMap< - Function, - {id: ServerReferenceId, bound: null | Thenable>}, -> = new WeakMap(); - -export function createServerReference, T>( - id: ServerReferenceId, - callServer: CallServerCallback, -): (...A) => Promise { - const proxy = function (): Promise { - // $FlowFixMe[method-unbinding] - const args = Array.prototype.slice.call(arguments); - return callServer(id, args); - }; - knownServerReferences.set(proxy, {id: id, bound: null}); - return proxy; -} diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMForm-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMForm-test.js new file mode 100644 index 0000000000000..31121cef82c52 --- /dev/null +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMForm-test.js @@ -0,0 +1,206 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @emails react-core + */ + +'use strict'; + +import {insertNodesAndExecuteScripts} from 'react-dom/src/test-utils/FizzTestUtils'; + +// Polyfills for test environment +global.ReadableStream = + require('web-streams-polyfill/ponyfill/es6').ReadableStream; +global.TextEncoder = require('util').TextEncoder; +global.TextDecoder = require('util').TextDecoder; + +let actionResult; +let container; +let serverExports; +let webpackServerMap; +let React; +let ReactDOMServer; +let ReactServerDOMServer; +let ReactServerDOMClient; + +describe('ReactFlightDOMReply', () => { + beforeEach(() => { + jest.resetModules(); + const WebpackMock = require('./utils/WebpackMock'); + serverExports = WebpackMock.serverExports; + webpackServerMap = WebpackMock.webpackServerMap; + React = require('react'); + ReactServerDOMServer = require('react-server-dom-webpack/server.browser'); + ReactServerDOMClient = require('react-server-dom-webpack/client'); + ReactDOMServer = require('react-dom/server.browser'); + container = document.createElement('div'); + document.body.appendChild(container); + actionResult = undefined; + }); + + afterEach(() => { + document.body.removeChild(container); + }); + + function requireServerRef(ref) { + let name = ''; + let resolvedModuleData = webpackServerMap[ref]; + if (resolvedModuleData) { + // The potentially aliased name. + name = resolvedModuleData.name; + } else { + // We didn't find this specific export name but we might have the * export + // which contains this name as well. + // TODO: It's unfortunate that we now have to parse this string. We should + // probably go back to encoding path and name separately on the client reference. + const idx = ref.lastIndexOf('#'); + if (idx !== -1) { + name = ref.slice(idx + 1); + resolvedModuleData = webpackServerMap[ref.slice(0, idx)]; + } + if (!resolvedModuleData) { + throw new Error( + 'Could not find the module "' + + ref + + '" in the React Client Manifest. ' + + 'This is probably a bug in the React Server Components bundler.', + ); + } + } + const mod = __webpack_require__(resolvedModuleData.id); + if (name === '*') { + return mod; + } + return mod[name]; + } + + function POST(formData) { + let actionId = null; + formData.forEach((v, key) => { + if (key.startsWith('$ACTION_') && actionId === null) { + actionId = key.slice(8); + } + }); + if (actionId === null) { + throw new Error('Missing action'); + } + const action = requireServerRef(actionId); + actionResult = action(formData); + } + + function submit(submitter) { + const form = submitter.form || submitter; + if (!submitter.form) { + submitter = undefined; + } + const submitEvent = new Event('submit', {bubbles: true, cancelable: true}); + submitEvent.submitter = submitter; + const returnValue = form.dispatchEvent(submitEvent); + if (!returnValue) { + return; + } + const action = + (submitter && submitter.getAttribute('formaction')) || form.action; + if (!/\s*javascript:/i.test(action)) { + const method = (submitter && submitter.formMethod) || form.method; + const encType = (submitter && submitter.formEnctype) || form.enctype; + if (method === 'post' && encType === 'multipart/form-data') { + let formData; + if (submitter) { + const temp = document.createElement('input'); + temp.name = submitter.name; + temp.value = submitter.value; + submitter.parentNode.insertBefore(temp, submitter); + formData = new FormData(form); + temp.parentNode.removeChild(temp); + } else { + formData = new FormData(form); + } + POST(formData); + return; + } + throw new Error('Navigate to: ' + action); + } + } + + async function readIntoContainer(stream) { + const reader = stream.getReader(); + let result = ''; + while (true) { + const {done, value} = await reader.read(); + if (done) { + break; + } + result += Buffer.from(value).toString('utf8'); + } + const temp = document.createElement('div'); + temp.innerHTML = result; + insertNodesAndExecuteScripts(temp, container, null); + } + + // @gate enableFormActions + it('can submit a passed server action without hydrating it', async () => { + let foo = null; + + const serverAction = serverExports(function action(formData) { + foo = formData.get('foo'); + return 'hello'; + }); + function App() { + return ( +
+ +
+ ); + } + const rscStream = ReactServerDOMServer.renderToReadableStream(); + const response = ReactServerDOMClient.createFromReadableStream(rscStream); + const ssrStream = await ReactDOMServer.renderToReadableStream(response); + await readIntoContainer(ssrStream); + + const form = container.firstChild; + + expect(foo).toBe(null); + + submit(form); + + expect(foo).toBe('bar'); + expect(await actionResult).toBe('hello'); + }); + + // @gate enableFormActions + it('can submit an imported server action without hydrating it', async () => { + let foo = null; + + const ServerModule = serverExports(function action(formData) { + foo = formData.get('foo'); + return 'hi'; + }); + const serverAction = ReactServerDOMClient.createServerReference( + ServerModule.$$id, + ); + function App() { + return ( +
+ +
+ ); + } + + const ssrStream = await ReactDOMServer.renderToReadableStream(); + await readIntoContainer(ssrStream); + + const form = container.firstChild; + + expect(foo).toBe(null); + + submit(form); + + expect(foo).toBe('bar'); + + expect(await actionResult).toBe('hi'); + }); +}); diff --git a/scripts/error-codes/codes.json b/scripts/error-codes/codes.json index e38f97f6c904d..b1ae802b33b56 100644 --- a/scripts/error-codes/codes.json +++ b/scripts/error-codes/codes.json @@ -465,5 +465,6 @@ "477": "React Internal Error: processHintChunk is not implemented for Native-Relay. The fact that this method was called means there is a bug in React.", "478": "Thenable should have already resolved. This is a bug in React.", "479": "Cannot update optimistic state while rendering.", - "480": "File/Blob fields are not yet supported in progressive forms. It probably means you are closing over binary data or FormData in a Server Action." + "480": "File/Blob fields are not yet supported in progressive forms. It probably means you are closing over binary data or FormData in a Server Action.", + "481": "Tried to encode a Server Action from a different instance than the encoder is from. This is a bug in React." } From 5a1b03318d757c5deac9497d9ee5c846797ace73 Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Tue, 2 May 2023 13:00:57 -0400 Subject: [PATCH 2/4] Encode action ids and closures automatically and expose decodeAction decodeAction lets you take a form that was posted back to the server and automatically decode the first action's server reference and its bound arguments. We must wait to fill up the FormData since we're going to pass them all as arguments to the action and we don't know how many there will be. --- .../src/ReactFlightReplyClient.js | 75 +++++++++- .../src/server/ReactFizzConfigDOM.js | 2 +- .../src/ReactFlightDOMServerBrowser.js | 4 +- .../src/ReactFlightDOMServerEdge.js | 4 +- .../src/ReactFlightDOMServerNode.js | 9 +- .../src/__tests__/ReactFlightDOMForm-test.js | 129 +++++++++++------- .../src/ReactFlightActionServer.js | 109 +++++++++++++++ 7 files changed, 274 insertions(+), 58 deletions(-) create mode 100644 packages/react-server/src/ReactFlightActionServer.js diff --git a/packages/react-client/src/ReactFlightReplyClient.js b/packages/react-client/src/ReactFlightReplyClient.js index 412c176e19e11..623a28760d4aa 100644 --- a/packages/react-client/src/ReactFlightReplyClient.js +++ b/packages/react-client/src/ReactFlightReplyClient.js @@ -23,6 +23,10 @@ import { } from 'shared/ReactSerializationErrors'; import isArray from 'shared/isArray'; +import type { + FulfilledThenable, + RejectedThenable, +} from '../../shared/ReactTypes'; type ReactJSONValue = | string @@ -367,6 +371,43 @@ export function processReply( } } +const boundCache: WeakMap< + {id: ServerReferenceId, bound: null | Thenable>}, + Thenable, +> = new WeakMap(); + +function encodeFormData(reference: any): Thenable { + let resolve, reject; + // We need to have a handle on the thenable so that we can synchronously set + // its status from processReply, when it can complete synchronously. + const thenable: Thenable = new Promise((res, rej) => { + resolve = res; + reject = rej; + }); + processReply( + reference, + '', + (body: string | FormData) => { + if (typeof body === 'string') { + const data = new FormData(); + data.append('0', body); + body = data; + } + const fulfilled: FulfilledThenable = (thenable: any); + fulfilled.status = 'fulfilled'; + fulfilled.value = body; + resolve(body); + }, + e => { + const rejected: RejectedThenable = (thenable: any); + rejected.status = 'rejected'; + rejected.reason = e; + reject(e); + }, + ); + return thenable; +} + export function encodeFormAction( this: any => Promise, identifierPrefix: string, @@ -378,11 +419,41 @@ export function encodeFormAction( 'This is a bug in React.', ); } + let data: null | FormData = null; + let name; + const boundPromise = reference.bound; + if (boundPromise !== null) { + let thenable = boundCache.get(reference); + if (!thenable) { + thenable = encodeFormData(reference); + boundCache.set(reference, thenable); + } + if (thenable.status === 'rejected') { + throw thenable.reason; + } else if (thenable.status !== 'fulfilled') { + throw thenable; + } + const encodedFormData = thenable.value; + // This is hacky but we need the identifier prefix to be added to + // all fields but the suspense cache would break since we might get + // a new identifier each time. So we just append it at the end instead. + const prefixedData = new FormData(); + // $FlowFixMe[prop-missing] + encodedFormData.forEach((value: string | File, key: string) => { + prefixedData.append('$ACTION_' + identifierPrefix + ':' + key, value); + }); + data = prefixedData; + // We encode the name of the prefix containing the data. + name = '$ACTION_REF_' + identifierPrefix; + } else { + // This is the simple case so we can just encode the ID. + name = '$ACTION_ID_' + reference.id; + } return { - name: '$ACTION_' + reference.id, + name: name, method: 'POST', encType: 'multipart/form-data', - data: null, + data: data, }; } diff --git a/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js b/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js index b0c23a5c8dea3..353a534090b69 100644 --- a/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js +++ b/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js @@ -672,7 +672,7 @@ function makeFormFieldPrefix(responseState: ResponseState): string { // I'm just reusing this counter. It's not really the same namespace as "name". // It could just be its own counter. const id = responseState.nextSuspenseID++; - return responseState.idPrefix + '$ACTION:' + id + ':'; + return responseState.idPrefix + id; } // Since this will likely be repeated a lot in the HTML, we use a more concise message diff --git a/packages/react-server-dom-webpack/src/ReactFlightDOMServerBrowser.js b/packages/react-server-dom-webpack/src/ReactFlightDOMServerBrowser.js index 777e4271e6e1e..24db03db06669 100644 --- a/packages/react-server-dom-webpack/src/ReactFlightDOMServerBrowser.js +++ b/packages/react-server-dom-webpack/src/ReactFlightDOMServerBrowser.js @@ -25,6 +25,8 @@ import { getRoot, } from 'react-server/src/ReactFlightReplyServer'; +import {decodeAction} from 'react-server/src/ReactFlightActionServer'; + type Options = { identifierPrefix?: string, signal?: AbortSignal, @@ -87,4 +89,4 @@ function decodeReply( return getRoot(response); } -export {renderToReadableStream, decodeReply}; +export {renderToReadableStream, decodeReply, decodeAction}; diff --git a/packages/react-server-dom-webpack/src/ReactFlightDOMServerEdge.js b/packages/react-server-dom-webpack/src/ReactFlightDOMServerEdge.js index 777e4271e6e1e..24db03db06669 100644 --- a/packages/react-server-dom-webpack/src/ReactFlightDOMServerEdge.js +++ b/packages/react-server-dom-webpack/src/ReactFlightDOMServerEdge.js @@ -25,6 +25,8 @@ import { getRoot, } from 'react-server/src/ReactFlightReplyServer'; +import {decodeAction} from 'react-server/src/ReactFlightActionServer'; + type Options = { identifierPrefix?: string, signal?: AbortSignal, @@ -87,4 +89,4 @@ function decodeReply( return getRoot(response); } -export {renderToReadableStream, decodeReply}; +export {renderToReadableStream, decodeReply, decodeAction}; diff --git a/packages/react-server-dom-webpack/src/ReactFlightDOMServerNode.js b/packages/react-server-dom-webpack/src/ReactFlightDOMServerNode.js index f23959b2f8bee..d98bf3baf1f9a 100644 --- a/packages/react-server-dom-webpack/src/ReactFlightDOMServerNode.js +++ b/packages/react-server-dom-webpack/src/ReactFlightDOMServerNode.js @@ -36,6 +36,8 @@ import { getRoot, } from 'react-server/src/ReactFlightReplyServer'; +import {decodeAction} from 'react-server/src/ReactFlightActionServer'; + function createDrainHandler(destination: Destination, request: Request) { return () => startFlowing(request, destination); } @@ -148,4 +150,9 @@ function decodeReply( return getRoot(response); } -export {renderToPipeableStream, decodeReplyFromBusboy, decodeReply}; +export { + renderToPipeableStream, + decodeReplyFromBusboy, + decodeReply, + decodeAction, +}; diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMForm-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMForm-test.js index 31121cef82c52..51d208bce3eb4 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMForm-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMForm-test.js @@ -17,7 +17,6 @@ global.ReadableStream = global.TextEncoder = require('util').TextEncoder; global.TextDecoder = require('util').TextDecoder; -let actionResult; let container; let serverExports; let webpackServerMap; @@ -38,57 +37,18 @@ describe('ReactFlightDOMReply', () => { ReactDOMServer = require('react-dom/server.browser'); container = document.createElement('div'); document.body.appendChild(container); - actionResult = undefined; }); afterEach(() => { document.body.removeChild(container); }); - function requireServerRef(ref) { - let name = ''; - let resolvedModuleData = webpackServerMap[ref]; - if (resolvedModuleData) { - // The potentially aliased name. - name = resolvedModuleData.name; - } else { - // We didn't find this specific export name but we might have the * export - // which contains this name as well. - // TODO: It's unfortunate that we now have to parse this string. We should - // probably go back to encoding path and name separately on the client reference. - const idx = ref.lastIndexOf('#'); - if (idx !== -1) { - name = ref.slice(idx + 1); - resolvedModuleData = webpackServerMap[ref.slice(0, idx)]; - } - if (!resolvedModuleData) { - throw new Error( - 'Could not find the module "' + - ref + - '" in the React Client Manifest. ' + - 'This is probably a bug in the React Server Components bundler.', - ); - } - } - const mod = __webpack_require__(resolvedModuleData.id); - if (name === '*') { - return mod; - } - return mod[name]; - } - - function POST(formData) { - let actionId = null; - formData.forEach((v, key) => { - if (key.startsWith('$ACTION_') && actionId === null) { - actionId = key.slice(8); - } - }); - if (actionId === null) { - throw new Error('Missing action'); - } - const action = requireServerRef(actionId); - actionResult = action(formData); + async function POST(formData) { + const boundAction = await ReactServerDOMServer.decodeAction( + formData, + webpackServerMap, + ); + return boundAction(); } function submit(submitter) { @@ -119,8 +79,7 @@ describe('ReactFlightDOMReply', () => { } else { formData = new FormData(form); } - POST(formData); - return; + return POST(formData); } throw new Error('Navigate to: ' + action); } @@ -165,10 +124,10 @@ describe('ReactFlightDOMReply', () => { expect(foo).toBe(null); - submit(form); + const result = await submit(form); + expect(result).toBe('hello'); expect(foo).toBe('bar'); - expect(await actionResult).toBe('hello'); }); // @gate enableFormActions @@ -197,10 +156,76 @@ describe('ReactFlightDOMReply', () => { expect(foo).toBe(null); - submit(form); + const result = await submit(form); + + expect(result).toBe('hi'); expect(foo).toBe('bar'); + }); + + // @gate enableFormActions + it('can submit a complex closure server action without hydrating it', async () => { + let foo = null; + + const serverAction = serverExports(function action(bound, formData) { + foo = formData.get('foo') + bound.complex; + return 'hello'; + }); + function App() { + return ( +
+ +
+ ); + } + const rscStream = ReactServerDOMServer.renderToReadableStream(); + const response = ReactServerDOMClient.createFromReadableStream(rscStream); + const ssrStream = await ReactDOMServer.renderToReadableStream(response); + await readIntoContainer(ssrStream); + + const form = container.firstChild; + + expect(foo).toBe(null); + + const result = await submit(form); + + expect(result).toBe('hello'); + expect(foo).toBe('barobject'); + }); + + // @gate enableFormActions + it('can submit a multiple complex closure server action without hydrating it', async () => { + let foo = null; + + const serverAction = serverExports(function action(bound, formData) { + foo = formData.get('foo') + bound.complex; + return 'hello' + bound.complex; + }); + function App() { + return ( +
+ +
); diff --git a/fixtures/flight/src/Form.js b/fixtures/flight/src/Form.js index c166f5a16bb15..d98452c68304a 100644 --- a/fixtures/flight/src/Form.js +++ b/fixtures/flight/src/Form.js @@ -14,11 +14,7 @@ export default function Form({action, children}) { return ( -
{ - const result = await action(formData); - alert(result); - }}> + diff --git a/fixtures/flight/src/ServerState.js b/fixtures/flight/src/ServerState.js new file mode 100644 index 0000000000000..3d4c7162262dc --- /dev/null +++ b/fixtures/flight/src/ServerState.js @@ -0,0 +1,9 @@ +let serverState = 'Hello World'; + +export function setServerState(message) { + serverState = message; +} + +export function getServerState() { + return serverState; +} diff --git a/fixtures/flight/src/actions.js b/fixtures/flight/src/actions.js index 87cba005e0b72..3d26189979c2f 100644 --- a/fixtures/flight/src/actions.js +++ b/fixtures/flight/src/actions.js @@ -1,11 +1,15 @@ 'use server'; +import {setServerState} from './ServerState.js'; + export async function like() { + setServerState('Liked!'); return new Promise((resolve, reject) => resolve('Liked')); } export async function greet(formData) { const name = formData.get('name') || 'you'; + setServerState('Hi ' + name); const file = formData.get('file'); if (file) { return `Ok, ${name}, here is ${file.name}: diff --git a/fixtures/flight/src/index.js b/fixtures/flight/src/index.js index 3e8b7e5bcce28..d75feee56ec36 100644 --- a/fixtures/flight/src/index.js +++ b/fixtures/flight/src/index.js @@ -1,11 +1,29 @@ import * as React from 'react'; -import {use, Suspense} from 'react'; +import {use, Suspense, useState, startTransition} from 'react'; import ReactDOM from 'react-dom/client'; import {createFromFetch, encodeReply} from 'react-server-dom-webpack/client'; // TODO: This should be a dependency of the App but we haven't implemented CSS in Node yet. import './style.css'; +let updateRoot; +async function callServer(id, args) { + const response = fetch('/', { + method: 'POST', + headers: { + Accept: 'text/x-component', + 'rsc-action': id, + }, + body: await encodeReply(args), + }); + const {returnValue, root} = await createFromFetch(response, {callServer}); + // Refresh the tree with the new RSC payload. + startTransition(() => { + updateRoot(root); + }); + return returnValue; +} + let data = createFromFetch( fetch('/', { headers: { @@ -13,22 +31,14 @@ let data = createFromFetch( }, }), { - async callServer(id, args) { - const response = fetch('/', { - method: 'POST', - headers: { - Accept: 'text/x-component', - 'rsc-action': id, - }, - body: await encodeReply(args), - }); - return createFromFetch(response); - }, + callServer, } ); function Shell({data}) { - return use(data); + const [root, setRoot] = useState(use(data)); + updateRoot = setRoot; + return root; } ReactDOM.hydrateRoot(document, ); From 249f02a6175a38ed7213c79f80436cbc4f42de24 Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Wed, 3 May 2023 15:26:46 -0700 Subject: [PATCH 4/4] This type is not really defined in the Reply/Action side --- packages/react-server/src/ReactFlightActionServer.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/react-server/src/ReactFlightActionServer.js b/packages/react-server/src/ReactFlightActionServer.js index 07e538fac1ece..ee735c2e8f9a2 100644 --- a/packages/react-server/src/ReactFlightActionServer.js +++ b/packages/react-server/src/ReactFlightActionServer.js @@ -10,7 +10,6 @@ import type {Thenable} from 'shared/ReactTypes'; import type { - ServerReferenceId, ServerManifest, ClientReference as ServerReference, } from 'react-client/src/ReactFlightClientConfig'; @@ -23,6 +22,8 @@ import { import {createResponse, close, getRoot} from './ReactFlightReplyServer'; +type ServerReferenceId = any; + function bindArgs(fn: any, args: any) { return fn.bind.apply(fn, [null].concat(args)); }