Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

useFormState: Reuse state from previous form submission #27321

Merged
merged 2 commits into from
Sep 13, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Update Flight fixture to useFormState
Updates the Flight fixture's Counter component to useFormState instead
of useState.
  • Loading branch information
acdlite committed Sep 13, 2023
commit 765ceb28ace12ec6a66ef462505a38383cca2b98
6 changes: 5 additions & 1 deletion fixtures/flight/server/global.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
15 changes: 9 additions & 6 deletions fixtures/flight/server/region.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
);
Expand Down Expand Up @@ -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) {
Expand All @@ -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) {
Expand Down Expand Up @@ -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;
Expand All @@ -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);
}
});

Expand Down
8 changes: 4 additions & 4 deletions fixtures/flight/src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -32,9 +32,9 @@ export default async function App() {
<body>
<Container>
<h1>{getServerState()}</h1>
<Counter />
<Counter2 />
<Counter3 />
<Counter incrementAction={increment} />
<Counter2 incrementAction={increment} />
<Counter3 incrementAction={increment} />
<ul>
{todos.map(todo => (
<li key={todo.id}>{todo.text}</li>
Expand Down
9 changes: 6 additions & 3 deletions fixtures/flight/src/Counter.js
Original file line number Diff line number Diff line change
@@ -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 (
<Container>
<button onClick={() => setCount(c => c + 1)}>Count: {count}</button>
<form>
<button formAction={incrementFormAction}>Count: {count}</button>
</form>
</Container>
);
}
4 changes: 4 additions & 0 deletions fixtures/flight/src/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,7 @@ export async function greet(formData) {
}
return 'Hi ' + name + '!';
}

export async function increment(n) {
return n + 1;
}
38 changes: 25 additions & 13 deletions fixtures/flight/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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, <Shell data={data} />);
async function hydrateApp() {
const {root, returnValue, formState} = await createFromFetch(
fetch('/', {
headers: {
Accept: 'text/x-component',
},
}),
{
callServer,
}
);

ReactDOM.hydrateRoot(document, <Shell data={root} />, {
// 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,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So since we're serializing formState (nor the rest of the RSC payload) submitting this without JS and then hydrating the result will result in a hydration mismatch in the fixture, right?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, yes. Didn't catch that because I disabled hydration entirely when I was testing. Should add a button or something to start hydration asynchronously.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not actually sure how to transport this since it's a separate process. Seems like it'd have to be encoded into the HTML.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea, Next.js encodes the stream as text into the html (which doesn't allow for binary). We didn't bother doing that here because we wanted a more built-in mechanism for this upstream anyway. Seems fine to leave with a comment for now.

});
}

// Remove this line to simulate MPA behavior
hydrateApp();