英文:
React Form being submitted at the wrong time
问题
我有两个按钮,一个类型是 "button",另一个类型是 "submit",都包裹在一个表单中,它们可以相互切换状态。奇怪的是,如果我点击 "button" 类型的按钮,表单会被提交,而如果我点击 "submit" 类型的按钮,表单则不会被提交。
我期望相反的情况发生,即点击 "button" 类型的按钮时不会提交表单,而点击 "submit" 类型的按钮时会提交表单。
英文:
I have two buttons, one of type "button" and one of type "submit", both wrapped in a form and which toggle each other. Weirdly, if I click on the button of type "button" the form is submitted and if I click of the button of type "submit" the form is not submitted.
<!-- begin snippet: js hide: false console: true babel: true -->
<!-- language: lang-js -->
const rootElement = document.getElementById('root');
const root = ReactDOM.createRoot(rootElement);
function App() {
const [clicked, setClicked] = React.useState(false);
return (
<form
onSubmit={(e) => {
e.preventDefault();
console.log("form submitted!");
}}
>
{!clicked ? (
<button type="button" onClick={() => setClicked(true)}>
Button 1
</button>
) : (
<button type="submit" onClick={() => setClicked(false)}>
Button 2
</button>
)}
</form>
);
}
root.render(
<App />
);
<!-- language: lang-html -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.production.min.js"></script>
<div id='root'></div>
<!-- end snippet -->
I would expect that the opposite be true in regards to submitting the form.
答案1
得分: 8
尝试这段代码,手动将showSubmitButton
状态的初始值设置为true
或false
,你会看到到目前为止,onSubmit
事件正在寻找一个类型为submit的输入来触发,一切都运行正常。
你还可以注意到在onSubmit
事件处理程序运行之前,组件重新渲染。
import { useState } from "react";
const App = () => {
const [counter, setCounter] = useState(0);
const [showSubmitButton, setShowSubmitButton] = useState(true);
return (
<>
{console.log("组件重新渲染,counter 为: ", counter)}
<form
onSubmit={(e) => {
console.log(e);
e.preventDefault();
console.log("表单提交!");
}}
>
{showSubmitButton ? (
<button
type="submit"
onClick={(e) => {
console.log("提交按钮被点击");
setCounter((prev) => prev + 1);
// setShowSubmitButton((prev) => !prev);
}}
>
提交
</button>
) : (
<button
type="button"
onClick={() => {
console.log("普通按钮被点击");
setCounter((prev) => prev + 1);
// setShowSubmitButton((prev) => !prev);
}}
>
按钮
</button>
)}
</form>
</>
);
};
export default App
当你取消注释提交按钮中的 setShowSubmitButton((prev) => !prev)
时,剧情开始变得复杂。当你点击它并切换 showSubmitButton
时,组件重新渲染,就像触发了 onSubmit
事件,但无法执行,因为组件重新渲染后,不能找到必需的类型为 submit 的输入,所以什么都不会发生,到目前为止,两个按钮都没有触发 onSubmit
事件。
现在取消注释普通按钮中的 setShowSubmitButton((prev) => !prev)
,你会发现当你点击该按钮时,onSubmit
事件会触发,如果你在 onSubmit
内部检查 e.target
,你会发现它等于:
<form>
<button type="submit">提交</button>
</form>
因此,当你点击提交按钮时,似乎 onSubmit
事件被卡住了,因为找不到类型为 submit 的输入,因此当你点击普通按钮时,类型为 submit 的输入重新出现在 DOM 中,所以事件处理程序最终可以找到它并运行。
我知道这很疯狂,但这是唯一的解释,没有办法让普通按钮触发 onSubmit
。如果将状态更新移动到 e.preventDefault()
之后的事件处理程序内部,你会看到它按预期工作,因为组件只有在 onSubmit
事件处理程序函数内部的代码完成后才会重新渲染。
英文:
Try this code, set manually the initial value of the showSubmitButton
state to either true
or false
and you'll see that so far so good, the onSubmit
event is looking for an input of type submit to fire and all works fine.<br>
you can also notice that the component rerenders before the onSubmit event handler runs.
import { useState } from "react";
const App = () => {
const [counter, setCounter] = useState(0);
const [showSubmitButton, setShowSubmitButton] = useState(true);
return (
<>
{console.log("component rerender and counter is: ", counter)}
<form
onSubmit={(e) => {
console.log(e);
e.preventDefault();
console.log("form submitted!");
}}
>
{showSubmitButton ? (
<button
type="submit"
onClick={(e) => {
console.log("submit button clicked");
setCounter((prev) => prev + 1);
// setShowSubmitButton((prev) => !prev);
}}
>
Submit
</button>
) : (
<button
type="button"
onClick={() => {
console.log("simple button clicked");
setCounter((prev) => prev + 1);
// setShowSubmitButton((prev) => !prev);
}}
>
Button
</button>
)}
</form>
</>
);
};
export default App
the drama begins when you uncomment setShowSubmitButton((prev) => !prev)
in the submit button.<br>
now when you click it and toggle showSubmitButton
, the component rerenders it is like the onSubmit
event is triggered but cannot fire because the component rerenders and the input of type submit which is mandatory to do so cannot be found so nothing happens, till now, neither one of the two buttons is triggering onSubmit
.<br>
now uncomment setShowSubmitButton((prev) => !prev)
in the simple button.<br> you'll see when you click that button the onSubmit
event is firing and if you check e.target
from inside onSubmit
you will find it equal to
<form>
<button type="submit">Submit</button>
</form>
so when you click the submit button, it seems like the onSubmit
event is stuck because the input of type submit cannot be found therefore when you click the simple button, the input of type submit is back to DOM, so the event handler can finally find it and run.<br>
I know this is crazy, but it is the only explanation, there is no way that the simple button is triggering onSubmit
.
if you move state updates inside the event handler after e.preventDefault()
:
<>
{console.log("component rerender and counter is: ", counter)}
<form
onSubmit={(e) => {
console.log(e);
e.preventDefault();
console.log("form submitted!");
setCounter((prev) => prev + 1);
setShowSubmitButton((prev) => !prev);
}}
>
{showSubmitButton ? (
<button
type="submit"
onClick={(e) => {
console.log("submit button clicked");
}}
>
Submit
</button>
) : (
<button
type="button"
onClick={() => {
console.log("simple button clicked");
}}
>
Button
</button>
)}
</form>
</>
);
you will see it working as expected! because the component will rerender only when the code inside the onSubmit
event handler function finishes
答案2
得分: 6
通过一些测试,我可以猜测这是因为在按钮交换之前触发的事件在按钮交换之后执行。我可以通过使提交按钮在点击后消失并重新出现来复现这个问题。
在你看到的示例中,如果按钮在点击后立即消失,表单就不会提交。事件可能在按钮不在DOM中时尝试执行,所以什么都没有发生。
至于为什么 'button' 类型会提交表单,这可能是因为事件执行之前,按钮类型可能已经更新为 'submit'。如果将两个按钮的类型都更改为 'button',表单就不会提交。
很难确定这是一个bug,还是虚拟DOM带来的性能问题。
至于解决方案,你可以使用零延迟的定时器来将状态更改推迟到事件队列的末尾,即在事件运行完成后。
有关零延迟的更多信息,请参考:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Event_loop#zero_delays
英文:
Through some testing I can guess this is because events that are triggered before the buttons swap are executing after the buttons swap. I can reproduce this by just making a submit button disappear and reappear after clicking.
<!-- begin snippet: js hide: true console: true babel: true -->
<!-- language: lang-js -->
const rootElement = document.getElementById('root');
const root = ReactDOM.createRoot(rootElement);
function RegularForm() {
return (
<form
onSubmit={(e) => {
e.preventDefault();
console.log('form submitted!');
}}
>
<p>Regular</p>
<button type="submit">Type "submit"</button>
</form>
);
}
function BuggedForm() {
const [show, setShow] = React.useState(true);
const onClick = () => {
setShow(false);
setTimeout(() => setShow(true));
};
return (
<form
onSubmit={(e) => {
e.preventDefault();
console.log('form submitted!');
}}
>
<p>Bugged</p>
{show && (
<button type="submit" onClick={onClick}>
Type "submit"
</button>
)}
</form>
);
}
root.render(
<div>
<RegularForm />
<BuggedForm />
</div>
);
<!-- language: lang-html -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.production.min.js"></script>
<div id='root'></div>
<!-- end snippet -->
As you can see, if the button disappears right after clicking, the form does not submit. The event probably tried to execute when the button was not present in the DOM, and nothing happened.
As for why the 'button' type submits the form, it's probably because the button type gets updated to 'submit' before the event executes. If you change both types to 'button' the form does not submit.
<!-- begin snippet: js hide: true console: true babel: true -->
<!-- language: lang-js -->
const rootElement = document.getElementById('root');
const root = ReactDOM.createRoot(rootElement);
function App() {
const [clicked, setClicked] = React.useState(false);
return (
<form
onSubmit={(e) => {
e.preventDefault();
console.log('form submitted!');
}}
>
{!clicked ? (
<button type="button" onClick={() => setClicked(true)}>
Button 1
</button>
) : (
<button type="button" onClick={() => setClicked(false)}>
Button 2
</button>
)}
</form>
);
}
root.render(
<App />
);
<!-- language: lang-html -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.production.min.js"></script>
<div id='root'></div>
<!-- end snippet -->
Hard to say whether this is a bug, or just jank that comes with the virtual DOM.
As for a solution, you can use a zero delay timeout to push the state change to the back of the queue. ie. after the events run their course.
<!-- begin snippet: js hide: true console: true babel: true -->
<!-- language: lang-js -->
const rootElement = document.getElementById('root');
const root = ReactDOM.createRoot(rootElement);
function App() {
const [clicked, setClicked] = React.useState(false);
const toggleClicked = () => setClicked((prev) => !prev);
// React Jank: Let form events finish before toggling
const onClick = () => setTimeout(toggleClicked);
return (
<form
onSubmit={(e) => {
e.preventDefault();
console.log('form submitted!');
}}
>
{!clicked ? (
<button type="button" onClick={onClick}>
Type 'button'
</button>
) : (
<button type="submit" onClick={onClick}>
Type 'submit'
</button>
)}
</form>
);
}
root.render(
<App />
);
<!-- language: lang-html -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.production.min.js"></script>
<div id='root'></div>
<!-- end snippet -->
More info on zero delays: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Event_loop#zero_delays
答案3
得分: 5
我不能完全解释这里发生了什么,但似乎——我不轻言其妙——这可能是 React 中的一个 bug。
一些观察:
-
如果在 onSubmit 处理程序中记录
event.nativeEvent.submitter
,你会看到按钮 2 提交了表单,尽管点击的是按钮 1。 -
如果将按钮 2 更改为
<input type="submit" value="Button 2" />
,它的行为就符合你的期望。 -
如果在按钮 1 的点击处理程序中调用
preventDefault
,则两个按钮都不会提交表单。 -
如果在
setTimeout
中包装setClicked
调用,它的行为就符合你的期望。 (见下面的示例代码。)
不确定这里发生了什么,但似乎在状态更新的重新渲染和点击事件的分发传播之间存在时间上的问题。 (我的直觉告诉我,会有比我聪明的人出现,并给出一个简单得多的解释,我会觉得愚蠢,因为我建议这可能是 React 中的一个 bug。)
这感觉有点像一种技巧,但如果你很急,可以将状态更新包装在 setTimeout 中修复它。
英文:
I can't fully explain what's happening here, but it seems like--and I don't say this lightly--it might be a bug in React.
A few observations:
-
If you log
event.nativeEvent.submitter
within the onSubmit handler you'll see Button 2 submitted the form despite having clicked Button 1. -
If you change Button 2 to
<input type="submit" value="Button 2" />
it behaves as you would expect. -
If you
preventDefault
in Button 1's click handler, neither button submits the form. -
If you wrap the
setClicked
calls in asetTimeout
it behaves as you'd expect. (See sample code below.)
Not sure what's going on here but it seems like there's a timing problem between the re-render from the state update and the dispatching and propagation of the click event. (My spidey-sense is telling me that someone smarter than me is going to come along with a much simpler explanation and I'm going to feel dumb for having suggested it's a bug in React.)
This feels like a bit of a hack, but if you're in a big hurry wrapping the state update in a setTimeout fixes it:
{!clicked ? (
<button
type="button"
onClick={() => {
setTimeout(() => setClicked(true), 1);
}}
>
Button 1
</button>
) : (
<button
type="submit"
onClick={(e) => {
setTimeout(() => setClicked(false), 1);
}}
>
Button 2
</button>
)}
答案4
得分: 5
以下是已翻译的内容:
- 第一点已由Ray解释,关于重新渲染问题。在“submit”事件处理之前,组件已经被渲染。上述语句的生成代码在此处。
- 第二点,也可能是主要问题,是在React表单中,第一个按钮的“click”事件被第二个按钮捕获,因为React认为它们是同一个按钮。可以通过为每个按钮添加“key属性”来区分它们,以阻止捕获其他按钮的点击事件。因此,如果您不在第二个按钮的onclick方法中更改状态,您将看到,如果在表单内点击任何按钮,它都会被提交。此语句的生成代码在此处。
- 要摆脱这个问题,只需为每个按钮添加“key属性”,这样React就不会混淆处理各个按钮的事件。此语句的生成代码在此处。
最后一个链接可能会解决您所有的问题。
如果有任何问题或我未能解释清楚的地方,请告诉我。
抱歉,我猜我的解释不太好。
英文:
There are multiple things to point out here. Let me try and explain.
- First point is already explained by Ray, about the re-render issue. The component is rendered before the submit event is handled for the "submit". Generated Code for the above statement - here
- Second and probably main point is that, in react form, click event of the first button is captured by the second button because react thinks that they are the same button. This can be avoided by adding the key property with each button to distinguish them and to stop capturing the click event for other button. So, if you don't change states in onclick method for the second button, you will see that, if you click any button inside a form, it will get submitted. Generated code for this statement - here
- To get rid of this, just add key property for each button, so that react doesn't get confused in handling event for the respective button. Generated code for this statement - here
The last link is probably going to clear all of your issues.
Let me know if there is any issue or something I failed to explain.
Sorry for the bad explanation, I guess.
答案5
得分: 3
如果移除条件运算符,则它将按预期工作 - type="submit" 实际上会提交表单。这表明按钮的类型在事件冒泡之前已经更新。这可能是一个错误。
英文:
If you remove the conditional operator, it is working as expected - the type="submit" actually submits the form. This suggests that the type of button updated before event bubbling. This might be a bug.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论