mirror of
https://github.com/hanzoai/react.git
synced 2026-09-24 17:02:27 +00:00
Restore server controlled form fields to whatever they should be (#26708)
Fizz can emit whatever it wants for the SSR version of these fields when it's a function action so they might not align with what is in the previous props. Therefore we need to force them to update if we're updating to a non-function where they might be relevant again.
This commit is contained in:
committed by
GitHub
parent
a6a29acf7a
commit
731237bea7
+118
-27
@@ -483,6 +483,68 @@ function setProp(
|
||||
case 'action':
|
||||
case 'formAction': {
|
||||
// TODO: Consider moving these special cases to the form, input and button tags.
|
||||
if (enableFormActions) {
|
||||
if (typeof value === 'function') {
|
||||
// Set a javascript URL that doesn't do anything. We don't expect this to be invoked
|
||||
// because we'll preventDefault, but it can happen if a form is manually submitted or
|
||||
// if someone calls stopPropagation before React gets the event.
|
||||
// If CSP is used to block javascript: URLs that's fine too. It just won't show this
|
||||
// error message but the URL will be logged.
|
||||
domElement.setAttribute(
|
||||
key,
|
||||
// eslint-disable-next-line no-script-url
|
||||
"javascript:throw new Error('" +
|
||||
'A React form was unexpectedly submitted. If you called form.submit() manually, ' +
|
||||
"consider using form.requestSubmit() instead. If you're trying to use " +
|
||||
'event.stopPropagation() in a submit event handler, consider also calling ' +
|
||||
'event.preventDefault().' +
|
||||
"')",
|
||||
);
|
||||
break;
|
||||
} else if (typeof prevValue === 'function') {
|
||||
// When we're switching off a Server Action that was originally hydrated.
|
||||
// The server control these fields during SSR that are now trailing.
|
||||
// The regular diffing doesn't apply since we compare against the previous props.
|
||||
// Instead, we need to force them to be set to whatever they should be now.
|
||||
// This would be a lot cleaner if we did this whole fork in the per-tag approach.
|
||||
if (key === 'formAction') {
|
||||
if (tag !== 'input') {
|
||||
// Setting the name here isn't completely safe for inputs if this is switching
|
||||
// to become a radio button. In that case we let the tag based override take
|
||||
// control.
|
||||
setProp(domElement, tag, 'name', props.name, props, null);
|
||||
}
|
||||
setProp(
|
||||
domElement,
|
||||
tag,
|
||||
'formEncType',
|
||||
props.formEncType,
|
||||
props,
|
||||
null,
|
||||
);
|
||||
setProp(
|
||||
domElement,
|
||||
tag,
|
||||
'formMethod',
|
||||
props.formMethod,
|
||||
props,
|
||||
null,
|
||||
);
|
||||
setProp(
|
||||
domElement,
|
||||
tag,
|
||||
'formTarget',
|
||||
props.formTarget,
|
||||
props,
|
||||
null,
|
||||
);
|
||||
} else {
|
||||
setProp(domElement, tag, 'encType', props.encType, props, null);
|
||||
setProp(domElement, tag, 'method', props.method, props, null);
|
||||
setProp(domElement, tag, 'target', props.target, props, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (
|
||||
value == null ||
|
||||
(!enableFormActions && typeof value === 'function') ||
|
||||
@@ -495,24 +557,6 @@ function setProp(
|
||||
if (__DEV__) {
|
||||
validateFormActionInDevelopment(tag, key, value, props);
|
||||
}
|
||||
if (enableFormActions && typeof value === 'function') {
|
||||
// Set a javascript URL that doesn't do anything. We don't expect this to be invoked
|
||||
// because we'll preventDefault, but it can happen if a form is manually submitted or
|
||||
// if someone calls stopPropagation before React gets the event.
|
||||
// If CSP is used to block javascript: URLs that's fine too. It just won't show this
|
||||
// error message but the URL will be logged.
|
||||
domElement.setAttribute(
|
||||
key,
|
||||
// eslint-disable-next-line no-script-url
|
||||
"javascript:throw new Error('" +
|
||||
'A React form was unexpectedly submitted. If you called form.submit() manually, ' +
|
||||
"consider using form.requestSubmit() instead. If you're trying to use " +
|
||||
'event.stopPropagation() in a submit event handler, consider also calling ' +
|
||||
'event.preventDefault().' +
|
||||
"')",
|
||||
);
|
||||
break;
|
||||
}
|
||||
// `setAttribute` with objects becomes only `[object]` in IE8/9,
|
||||
// ('' + value) makes it output the correct toString()-value.
|
||||
if (__DEV__) {
|
||||
@@ -1138,7 +1182,7 @@ export function setInitialProperties(
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
setProp(domElement, tag, propKey, propValue, props);
|
||||
setProp(domElement, tag, propKey, propValue, props, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1169,7 +1213,7 @@ export function setInitialProperties(
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
setProp(domElement, tag, propKey, propValue, props);
|
||||
setProp(domElement, tag, propKey, propValue, props, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1935,7 +1979,14 @@ export function updatePropertiesWithDiff(
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
setProp(domElement, tag, propKey, propValue, nextProps, null);
|
||||
setProp(
|
||||
domElement,
|
||||
tag,
|
||||
propKey,
|
||||
propValue,
|
||||
nextProps,
|
||||
lastProps[propKey],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2010,7 +2061,14 @@ export function updatePropertiesWithDiff(
|
||||
}
|
||||
// defaultValue are ignored by setProp
|
||||
default: {
|
||||
setProp(domElement, tag, propKey, propValue, nextProps, null);
|
||||
setProp(
|
||||
domElement,
|
||||
tag,
|
||||
propKey,
|
||||
propValue,
|
||||
nextProps,
|
||||
lastProps[propKey],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2045,7 +2103,14 @@ export function updatePropertiesWithDiff(
|
||||
}
|
||||
// defaultValue is ignored by setProp
|
||||
default: {
|
||||
setProp(domElement, tag, propKey, propValue, nextProps, null);
|
||||
setProp(
|
||||
domElement,
|
||||
tag,
|
||||
propKey,
|
||||
propValue,
|
||||
nextProps,
|
||||
lastProps[propKey],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2066,7 +2131,14 @@ export function updatePropertiesWithDiff(
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
setProp(domElement, tag, propKey, propValue, nextProps, null);
|
||||
setProp(
|
||||
domElement,
|
||||
tag,
|
||||
propKey,
|
||||
propValue,
|
||||
nextProps,
|
||||
lastProps[propKey],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2105,7 +2177,14 @@ export function updatePropertiesWithDiff(
|
||||
}
|
||||
// defaultChecked and defaultValue are ignored by setProp
|
||||
default: {
|
||||
setProp(domElement, tag, propKey, propValue, nextProps, null);
|
||||
setProp(
|
||||
domElement,
|
||||
tag,
|
||||
propKey,
|
||||
propValue,
|
||||
nextProps,
|
||||
lastProps[propKey],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2122,7 +2201,7 @@ export function updatePropertiesWithDiff(
|
||||
propKey,
|
||||
propValue,
|
||||
nextProps,
|
||||
null,
|
||||
lastProps[propKey],
|
||||
);
|
||||
}
|
||||
return;
|
||||
@@ -2134,7 +2213,7 @@ export function updatePropertiesWithDiff(
|
||||
for (let i = 0; i < updatePayload.length; i += 2) {
|
||||
const propKey = updatePayload[i];
|
||||
const propValue = updatePayload[i + 1];
|
||||
setProp(domElement, tag, propKey, propValue, nextProps, null);
|
||||
setProp(domElement, tag, propKey, propValue, nextProps, lastProps[propKey]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2709,6 +2788,18 @@ function diffHydratedGenericElement(
|
||||
const hasFormActionURL = serverValue === EXPECTED_FORM_ACTION_URL;
|
||||
if (typeof value === 'function') {
|
||||
extraAttributes.delete(propKey.toLowerCase());
|
||||
// The server can set these extra properties to implement actions.
|
||||
// So we remove them from the extra attributes warnings.
|
||||
if (propKey === 'formAction') {
|
||||
extraAttributes.delete('name');
|
||||
extraAttributes.delete('formenctype');
|
||||
extraAttributes.delete('formmethod');
|
||||
extraAttributes.delete('formtarget');
|
||||
} else {
|
||||
extraAttributes.delete('enctype');
|
||||
extraAttributes.delete('method');
|
||||
extraAttributes.delete('target');
|
||||
}
|
||||
if (hasFormActionURL) {
|
||||
// Expected
|
||||
continue;
|
||||
|
||||
@@ -131,7 +131,6 @@ export function updateInput(
|
||||
// Submit/reset inputs need the attribute removed completely to avoid
|
||||
// blank-text buttons.
|
||||
node.removeAttribute('value');
|
||||
return;
|
||||
}
|
||||
|
||||
if (disableInputAttributeSyncing) {
|
||||
|
||||
@@ -195,4 +195,169 @@ describe('ReactDOMFizzForm', () => {
|
||||
'Prop `action` did not match. Server: "action" Client: "function action(formData) {}"',
|
||||
);
|
||||
});
|
||||
|
||||
// @gate enableFormActions || !__DEV__
|
||||
it('should reset form fields after you update away from hydrated function', async () => {
|
||||
const formRef = React.createRef();
|
||||
const inputRef = React.createRef();
|
||||
const buttonRef = React.createRef();
|
||||
function action(formData) {}
|
||||
function App({isUpdate}) {
|
||||
return (
|
||||
<form
|
||||
action={isUpdate ? 'action' : action}
|
||||
ref={formRef}
|
||||
method={isUpdate ? 'POST' : null}>
|
||||
<input
|
||||
type="submit"
|
||||
formAction={isUpdate ? 'action' : action}
|
||||
ref={inputRef}
|
||||
formTarget={isUpdate ? 'elsewhere' : null}
|
||||
/>
|
||||
<button
|
||||
formAction={isUpdate ? 'action' : action}
|
||||
ref={buttonRef}
|
||||
formEncType={isUpdate ? 'multipart/form-data' : null}
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
const stream = await ReactDOMServer.renderToReadableStream(<App />);
|
||||
await readIntoContainer(stream);
|
||||
let root;
|
||||
await act(async () => {
|
||||
root = ReactDOMClient.hydrateRoot(container, <App />);
|
||||
});
|
||||
await act(async () => {
|
||||
root.render(<App isUpdate={true} />);
|
||||
});
|
||||
expect(formRef.current.getAttribute('action')).toBe('action');
|
||||
expect(formRef.current.hasAttribute('encType')).toBe(false);
|
||||
expect(formRef.current.getAttribute('method')).toBe('POST');
|
||||
expect(formRef.current.hasAttribute('target')).toBe(false);
|
||||
|
||||
expect(inputRef.current.getAttribute('formAction')).toBe('action');
|
||||
expect(inputRef.current.hasAttribute('name')).toBe(false);
|
||||
expect(inputRef.current.hasAttribute('formEncType')).toBe(false);
|
||||
expect(inputRef.current.hasAttribute('formMethod')).toBe(false);
|
||||
expect(inputRef.current.getAttribute('formTarget')).toBe('elsewhere');
|
||||
|
||||
expect(buttonRef.current.getAttribute('formAction')).toBe('action');
|
||||
expect(buttonRef.current.hasAttribute('name')).toBe(false);
|
||||
expect(buttonRef.current.getAttribute('formEncType')).toBe(
|
||||
'multipart/form-data',
|
||||
);
|
||||
expect(buttonRef.current.hasAttribute('formMethod')).toBe(false);
|
||||
expect(buttonRef.current.hasAttribute('formTarget')).toBe(false);
|
||||
});
|
||||
|
||||
// @gate enableFormActions || !__DEV__
|
||||
it('should reset form fields after you remove a hydrated function', async () => {
|
||||
const formRef = React.createRef();
|
||||
const inputRef = React.createRef();
|
||||
const buttonRef = React.createRef();
|
||||
function action(formData) {}
|
||||
function App({isUpdate}) {
|
||||
return (
|
||||
<form action={isUpdate ? undefined : action} ref={formRef}>
|
||||
<input
|
||||
type="submit"
|
||||
formAction={isUpdate ? undefined : action}
|
||||
ref={inputRef}
|
||||
/>
|
||||
<button formAction={isUpdate ? undefined : action} ref={buttonRef} />
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
const stream = await ReactDOMServer.renderToReadableStream(<App />);
|
||||
await readIntoContainer(stream);
|
||||
let root;
|
||||
await act(async () => {
|
||||
root = ReactDOMClient.hydrateRoot(container, <App />);
|
||||
});
|
||||
await act(async () => {
|
||||
root.render(<App isUpdate={true} />);
|
||||
});
|
||||
expect(formRef.current.hasAttribute('action')).toBe(false);
|
||||
expect(formRef.current.hasAttribute('encType')).toBe(false);
|
||||
expect(formRef.current.hasAttribute('method')).toBe(false);
|
||||
expect(formRef.current.hasAttribute('target')).toBe(false);
|
||||
|
||||
expect(inputRef.current.hasAttribute('formAction')).toBe(false);
|
||||
expect(inputRef.current.hasAttribute('name')).toBe(false);
|
||||
expect(inputRef.current.hasAttribute('formEncType')).toBe(false);
|
||||
expect(inputRef.current.hasAttribute('formMethod')).toBe(false);
|
||||
expect(inputRef.current.hasAttribute('formTarget')).toBe(false);
|
||||
|
||||
expect(buttonRef.current.hasAttribute('formAction')).toBe(false);
|
||||
expect(buttonRef.current.hasAttribute('name')).toBe(false);
|
||||
expect(buttonRef.current.hasAttribute('formEncType')).toBe(false);
|
||||
expect(buttonRef.current.hasAttribute('formMethod')).toBe(false);
|
||||
expect(buttonRef.current.hasAttribute('formTarget')).toBe(false);
|
||||
});
|
||||
|
||||
// @gate enableFormActions || !__DEV__
|
||||
it('should restore the form fields even if they were incorrectly set', async () => {
|
||||
const formRef = React.createRef();
|
||||
const inputRef = React.createRef();
|
||||
const buttonRef = React.createRef();
|
||||
function action(formData) {}
|
||||
function App({isUpdate}) {
|
||||
return (
|
||||
<form
|
||||
action={isUpdate ? 'action' : action}
|
||||
ref={formRef}
|
||||
method="DELETE">
|
||||
<input
|
||||
type="submit"
|
||||
formAction={isUpdate ? 'action' : action}
|
||||
ref={inputRef}
|
||||
formTarget="elsewhere"
|
||||
/>
|
||||
<button
|
||||
formAction={isUpdate ? 'action' : action}
|
||||
ref={buttonRef}
|
||||
formEncType="text/plain"
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
// Specifying the extra form fields are a DEV error, but we expect it
|
||||
// to eventually still be patched up after an update.
|
||||
await expect(async () => {
|
||||
const stream = await ReactDOMServer.renderToReadableStream(<App />);
|
||||
await readIntoContainer(stream);
|
||||
}).toErrorDev([
|
||||
'Cannot specify a encType or method for a form that specifies a function as the action.',
|
||||
'Cannot specify a formTarget for a button that specifies a function as a formAction.',
|
||||
]);
|
||||
let root;
|
||||
await expect(async () => {
|
||||
await act(async () => {
|
||||
root = ReactDOMClient.hydrateRoot(container, <App />);
|
||||
});
|
||||
}).toErrorDev(['Prop `formTarget` did not match.']);
|
||||
await act(async () => {
|
||||
root.render(<App isUpdate={true} />);
|
||||
});
|
||||
expect(formRef.current.getAttribute('action')).toBe('action');
|
||||
expect(formRef.current.hasAttribute('encType')).toBe(false);
|
||||
expect(formRef.current.getAttribute('method')).toBe('DELETE');
|
||||
expect(formRef.current.hasAttribute('target')).toBe(false);
|
||||
|
||||
expect(inputRef.current.getAttribute('formAction')).toBe('action');
|
||||
expect(inputRef.current.hasAttribute('name')).toBe(false);
|
||||
expect(inputRef.current.hasAttribute('formEncType')).toBe(false);
|
||||
expect(inputRef.current.hasAttribute('formMethod')).toBe(false);
|
||||
expect(inputRef.current.getAttribute('formTarget')).toBe('elsewhere');
|
||||
|
||||
expect(buttonRef.current.getAttribute('formAction')).toBe('action');
|
||||
expect(buttonRef.current.hasAttribute('name')).toBe(false);
|
||||
expect(buttonRef.current.getAttribute('formEncType')).toBe('text/plain');
|
||||
expect(buttonRef.current.hasAttribute('formMethod')).toBe(false);
|
||||
expect(buttonRef.current.hasAttribute('formTarget')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user