Form Event Fires on Button Render : A Pesky Gotcha in React's Rendering Process
Kaho Shibuya
Posted on May 25, 2021
This post is a note that explains the issue and its cause and solution(s).
I created the web app that fetches the users' information and shows them as a list. It also has the functions to edit or delete them.
The final code is here.
What is the issue?
The issue was that the edit button seemed not working.
The code of the component with the issue is here.
You can also interact with the code here.
What causes?
Actually, the edit button works fine.
The reason why it seemed not working is because the edit button's onClick
event ends after the component gets re-rendered.
Inspection
Added console.log
and checked what happens when clicking the edit button.
loaded! // the page loaded
editComment is now: false // initial state
// click the edit button
Edit button is clicked!
editComment is now: true
handleSave is called!
editComment is now: false
According to the logs, the following happens under the hood.
- the edit button is clicked.
- the edit button's
onClick
event runs and updates stateeditComment
which is nowtrue
. (It wasfalse
as an initial state) - the component gets re-rendered.
-
handleSave
function is executed for some reason and updates stateeditComment
back tofalse
. - the component gets re-rendered.
The edit button works but the save button, I mean, handleSave
function gets executed at the same time.
Since these things happen very quickly, we cannot see it and it looks the edit button is not working.
The following code is the simplified version of the render part of the Comment component.
render(){
return this.state.editComment ? (
<tr>
<td><form id="form1" onSubmit={this.handleSave}></form></td>
<td><input form="form1" type="text" name="name"/></td>
<td><input form="form1" type="email" name="email"/></td>
<td><input form="form1" type="text" name="body" /></td>
<td><button form="form1" type="submit">Save</button></td>
</tr>
):(
<tr>
<td />
<td>{this.state.name}</td>
<td>{this.state.email}</td>
<td>{this.state.body}</td>
<td>
<button onClick={() => this.setState({ editComment: true })}>Edit</button>
<button onClick={() => handleDelete()}>Delete</button>
</td>
</tr>
)
}
state editComment
is false
at first, so there shouldn't be form
and the save button yet.
Weird!
Then why is handleSave
function called?
Again, it is because the edit button's onClick
event ends after the component gets re-rendered.
Facts
After clicking the edit button, form
gets created.
Since the edit button and the save button lie in the similar structure, so React regards these two as the DOM elements of the same type. In other word, React cannot differentiate these two buttons.
// simplified version
// before re-render
<tr>
<td />
<td>
<button onClick={() => this.setState({ editComment: true })}>Edit</button>
<button onClick={() => handleDelete()}>Delete</button>
</td>
</tr>
// after re-render
<tr>
<td>
<form id="form1" onSubmit={this.handleSave}></form>
</td>
<td>
<button form="form1" type="submit">Save</button>
</td>
</tr>
When comparing two React DOM elements of the same type, React looks at the attributes of both, keeps the same underlying DOM node, and only updates the changed attributes.
https://reactjs.org/docs/reconciliation.html#dom-elements-of-the-same-type
So, the edit button is not destroyed. It remains there and just gets updated its attributes and properties.
It is still the edit button with extra attributes such as from="form1"
or type="submit"
saying "save", so to speak.
Then still the button's onClick
persists.
When the button's onClick
event ends, the button is associated with form
and calls handleSave
function.
Solution(s)
Add
e.preventDefault()
to the edit button'sonClick
.
It won't callonSubmit
(=handleSave
function) inform
.Create new components for each DOM underlying the condition inside
render()
.
When the component is re-rendered, the new button(= the save button) is created rather than updating the existed button(= the edit button).
The edit button'sonClick
event is no longer listened.Add
key
to the edit button and save button respectively.
Inform React that these two buttons are different by addingkey
.
https://reactjs.org/docs/reconciliation.html#keys
Apparently, this is a super niche edge case.
Using a table layout or placing the form's items outside form
may cause the issue.
Considering accessibility or readability carefully when building the structure could prevent errors.
This is the lesson that I learned this time!
Acknowledgements
To understand this issue clearly, I popped into a bunch of web dev communities and asked around for this.
I'm really grateful to people in these communities for trying to help me with this. Again, thank you so much๐
Special thanks to Kohei Asai, Daniel Corner, Brandon Tsang, Shogo Wada.
Posted on May 25, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 28, 2024
November 27, 2024