diff --git a/fixtures/flight/server/global.js b/fixtures/flight/server/global.js index 16184287b1228..70ccb05ca83ac 100644 --- a/fixtures/flight/server/global.js +++ b/fixtures/flight/server/global.js @@ -138,11 +138,15 @@ app.all('/', async function (req, res, next) { // For HTML, we're a "client" emulator that runs the client code, // so we start by consuming the RSC payload. This needs a module // map that reverse engineers the client-side path to the SSR path. - const root = await createFromNodeStream(rscResponse, moduleMap); + const {root, formState} = await createFromNodeStream( + rscResponse, + moduleMap + ); // Render it into HTML by resolving the client components res.set('Content-type', 'text/html'); const {pipe} = renderToPipeableStream(root, { bootstrapScripts: mainJSChunks, + experimental_formState: formState, }); pipe(res); } catch (e) { diff --git a/fixtures/flight/server/region.js b/fixtures/flight/server/region.js index 4beae03ff35ee..e89f113b7cad6 100644 --- a/fixtures/flight/server/region.js +++ b/fixtures/flight/server/region.js @@ -46,7 +46,7 @@ const {readFile} = require('fs').promises; const React = require('react'); -async function renderApp(res, returnValue) { +async function renderApp(res, returnValue, formState) { const {renderToPipeableStream} = await import( 'react-server-dom-webpack/server' ); @@ -93,13 +93,13 @@ async function renderApp(res, returnValue) { React.createElement(App), ]; // For client-invoked server actions we refresh the tree and return a return value. - const payload = returnValue ? {returnValue, root} : root; + const payload = {root, returnValue, formState}; const {pipe} = renderToPipeableStream(payload, moduleMap); pipe(res); } app.get('/', async function (req, res) { - await renderApp(res, null); + await renderApp(res, null, null); }); app.post('/', bodyParser.text(), async function (req, res) { @@ -108,6 +108,7 @@ app.post('/', bodyParser.text(), async function (req, res) { decodeReply, decodeReplyFromBusboy, decodeAction, + decodeFormState, } = await import('react-server-dom-webpack/server'); const serverReference = req.get('rsc-action'); if (serverReference) { @@ -139,7 +140,7 @@ app.post('/', bodyParser.text(), async function (req, res) { // We handle the error on the client } // Refresh the client and return the value - renderApp(res, result); + renderApp(res, result, null); } else { // This is the progressive enhancement case const UndiciRequest = require('undici').Request; @@ -153,12 +154,14 @@ app.post('/', bodyParser.text(), async function (req, res) { const action = await decodeAction(formData); try { // Wait for any mutations - await action(); + const result = await action(); + const formState = decodeFormState(result, formData); + renderApp(res, null, formState); } catch (x) { const {setServerState} = await import('../src/ServerState.js'); setServerState('Error: ' + x.message); + renderApp(res, null, null); } - renderApp(res, null); } }); diff --git a/fixtures/flight/src/App.js b/fixtures/flight/src/App.js index 71eb4fa5d97a0..7a14beb460def 100644 --- a/fixtures/flight/src/App.js +++ b/fixtures/flight/src/App.js @@ -15,7 +15,7 @@ import {Client} from './Client.js'; import {Note} from './cjs/Note.js'; -import {like, greet} from './actions.js'; +import {like, greet, increment} from './actions.js'; import {getServerState} from './ServerState.js'; @@ -32,9 +32,9 @@ export default async function App() {

{getServerState()}

- - - + + +
    {todos.map(todo => (
  • {todo.text}
  • diff --git a/fixtures/flight/src/Counter.js b/fixtures/flight/src/Counter.js index 8785424ca469f..5af74369a1eb8 100644 --- a/fixtures/flight/src/Counter.js +++ b/fixtures/flight/src/Counter.js @@ -1,14 +1,17 @@ 'use client'; import * as React from 'react'; +import {experimental_useFormState as useFormState} from 'react-dom'; import Container from './Container.js'; -export function Counter() { - const [count, setCount] = React.useState(0); +export function Counter({incrementAction}) { + const [count, incrementFormAction] = useFormState(incrementAction, 0); return ( - +
    + +
    ); } diff --git a/fixtures/flight/src/actions.js b/fixtures/flight/src/actions.js index 3d26189979c2f..aa19871a9dcbb 100644 --- a/fixtures/flight/src/actions.js +++ b/fixtures/flight/src/actions.js @@ -18,3 +18,7 @@ export async function greet(formData) { } return 'Hi ' + name + '!'; } + +export async function increment(n) { + return n + 1; +} diff --git a/fixtures/flight/src/index.js b/fixtures/flight/src/index.js index d75feee56ec36..dd5a6b02b7681 100644 --- a/fixtures/flight/src/index.js +++ b/fixtures/flight/src/index.js @@ -24,21 +24,33 @@ async function callServer(id, args) { return returnValue; } -let data = createFromFetch( - fetch('/', { - headers: { - Accept: 'text/x-component', - }, - }), - { - callServer, - } -); - function Shell({data}) { - const [root, setRoot] = useState(use(data)); + const [root, setRoot] = useState(data); updateRoot = setRoot; return root; } -ReactDOM.hydrateRoot(document, ); +async function hydrateApp() { + const {root, returnValue, formState} = await createFromFetch( + fetch('/', { + headers: { + Accept: 'text/x-component', + }, + }), + { + callServer, + } + ); + + ReactDOM.hydrateRoot(document, , { + // TODO: This part doesn't actually work because the server only returns + // form state during the request that submitted the form. Which means it + // the state needs to be transported as part of the HTML stream. We intend + // to add a feature to Fizz for this, but for now it's up to the + // metaframework to implement correctly. + experimental_formState: formState, + }); +} + +// Remove this line to simulate MPA behavior +hydrateApp();