Skip to content

Commit 0a37151

Browse files
committed
Recover from errors thrown by progressive enhancement form generation
This would be from encoding previous state or bound/closures. In this case we fallback to event replaying instead of erroring the page.
1 parent a493901 commit 0a37151

File tree

2 files changed

+101
-11
lines changed

2 files changed

+101
-11
lines changed

packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js

+33-11
Original file line numberDiff line numberDiff line change
@@ -1045,14 +1045,40 @@ function pushAdditionalFormField(
10451045

10461046
function pushAdditionalFormFields(
10471047
target: Array<Chunk | PrecomputedChunk>,
1048-
formData: null | FormData,
1048+
formData: void | null | FormData,
10491049
) {
1050-
if (formData !== null) {
1050+
if (formData != null) {
10511051
// $FlowFixMe[prop-missing]: FormData has forEach.
10521052
formData.forEach(pushAdditionalFormField, target);
10531053
}
10541054
}
10551055

1056+
function getCustomFormFields(
1057+
resumableState: ResumableState,
1058+
formAction: any,
1059+
): null | ReactCustomFormAction {
1060+
const customAction = formAction.$$FORM_ACTION;
1061+
if (typeof customAction === 'function') {
1062+
const prefix = makeFormFieldPrefix(resumableState);
1063+
try {
1064+
return formAction.$$FORM_ACTION(prefix);
1065+
} catch (x) {
1066+
if (typeof x === 'object' && x !== null && typeof x.then === 'function') {
1067+
// Rethrow suspense.
1068+
throw x;
1069+
}
1070+
// If we fail to encode the form action for progressive enhancement for some reason,
1071+
// fallback to trying replaying on the client instead of failing the page. It might
1072+
// work there.
1073+
if (__DEV__) {
1074+
// TODO: Should this be some kind of recoverable error?
1075+
console['error']('Failed to serialize an action for progressive enhancement:\n%o', x);
1076+
}
1077+
}
1078+
}
1079+
return null;
1080+
}
1081+
10561082
function pushFormActionAttribute(
10571083
target: Array<Chunk | PrecomputedChunk>,
10581084
resumableState: ResumableState,
@@ -1062,7 +1088,7 @@ function pushFormActionAttribute(
10621088
formMethod: any,
10631089
formTarget: any,
10641090
name: any,
1065-
): null | FormData {
1091+
): void | null | FormData {
10661092
let formData = null;
10671093
if (enableFormActions && typeof formAction === 'function') {
10681094
// Function form actions cannot control the form properties
@@ -1092,12 +1118,10 @@ function pushFormActionAttribute(
10921118
);
10931119
}
10941120
}
1095-
const customAction: ReactCustomFormAction = formAction.$$FORM_ACTION;
1096-
if (typeof customAction === 'function') {
1121+
const customFields = getCustomFormFields(resumableState, formAction);
1122+
if (customFields !== null) {
10971123
// This action has a custom progressive enhancement form that can submit the form
10981124
// back to the server if it's invoked before hydration. Such as a Server Action.
1099-
const prefix = makeFormFieldPrefix(resumableState);
1100-
const customFields = formAction.$$FORM_ACTION(prefix);
11011125
name = customFields.name;
11021126
formAction = customFields.action || '';
11031127
formEncType = customFields.encType;
@@ -1882,12 +1906,10 @@ function pushStartForm(
18821906
);
18831907
}
18841908
}
1885-
const customAction: ReactCustomFormAction = formAction.$$FORM_ACTION;
1886-
if (typeof customAction === 'function') {
1909+
const customFields = getCustomFormFields(resumableState, formAction);
1910+
if (customFields !== null) {
18871911
// This action has a custom progressive enhancement form that can submit the form
18881912
// back to the server if it's invoked before hydration. Such as a Server Action.
1889-
const prefix = makeFormFieldPrefix(resumableState);
1890-
const customFields = formAction.$$FORM_ACTION(prefix);
18911913
formAction = customFields.action || '';
18921914
formEncType = customFields.encType;
18931915
formMethod = customFields.method;

packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMForm-test.js

+68
Original file line numberDiff line numberDiff line change
@@ -897,4 +897,72 @@ describe('ReactFlightDOMForm', () => {
897897

898898
expect(form.action).toBe('http://localhost/permalink');
899899
});
900+
901+
// @gate enableFormActions
902+
// @gate enableAsyncActions
903+
it('useFormState can return JSX state during MPA form submission', async () => {
904+
const serverAction = serverExports(
905+
async function action(prevState, formData) {
906+
return <div>error message</div>;
907+
},
908+
);
909+
910+
function Form({action}) {
911+
const [errorMsg, dispatch] = useFormState(action, null);
912+
return <form action={dispatch}>{errorMsg}</form>;
913+
}
914+
915+
const FormRef = await clientExports(Form);
916+
917+
const rscStream = ReactServerDOMServer.renderToReadableStream(
918+
<FormRef action={serverAction} />,
919+
webpackMap,
920+
);
921+
const response = ReactServerDOMClient.createFromReadableStream(rscStream, {
922+
ssrManifest: {
923+
moduleMap: null,
924+
moduleLoading: null,
925+
},
926+
});
927+
const ssrStream = await ReactDOMServer.renderToReadableStream(response);
928+
await readIntoContainer(ssrStream);
929+
930+
const form1 = container.getElementsByTagName('form')[0];
931+
expect(form1.textContent).toBe('');
932+
933+
async function submitTheForm() {
934+
const form = container.getElementsByTagName('form')[0];
935+
const {formState} = await submit(form);
936+
937+
// Simulate an MPA form submission by resetting the container and
938+
// rendering again.
939+
container.innerHTML = '';
940+
941+
const postbackRscStream = ReactServerDOMServer.renderToReadableStream(
942+
<FormRef action={serverAction} />,
943+
webpackMap,
944+
);
945+
const postbackResponse = ReactServerDOMClient.createFromReadableStream(
946+
postbackRscStream,
947+
{
948+
ssrManifest: {
949+
moduleMap: null,
950+
moduleLoading: null,
951+
},
952+
},
953+
);
954+
const postbackSsrStream = await ReactDOMServer.renderToReadableStream(
955+
postbackResponse,
956+
{formState: formState},
957+
);
958+
await readIntoContainer(postbackSsrStream);
959+
}
960+
961+
await submitTheForm();
962+
963+
// The error message was returned as JSX.
964+
const form2 = container.getElementsByTagName('form')[0];
965+
expect(form2.textContent).toBe('error message');
966+
expect(form2.firstChild.tagName).toBe('DIV');
967+
});
900968
});

0 commit comments

Comments
 (0)