DEV Community

loading...
Cover image for Form Event Fires on Button Render : A Pesky Gotcha in React's Rendering Process

Form Event Fires on Button Render : A Pesky Gotcha in React's Rendering Process

Kaho Shibuya
I'm currently a student learning web development. Focusing on JavaScript now.
・4 min read

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.

Alt Text

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 
Enter fullscreen mode Exit fullscreen mode

According to the logs, the following happens under the hood.

  1. the edit button is clicked.
  2. the edit button's onClick event runs and updates state editComment which is now true. (It was false as an initial state)
  3. the component gets re-rendered.
  4. handleSave function is executed for some reason and updates state editComment back to false.
  5. 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>
  )
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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)

  1. Add e.preventDefault() to the edit button's onClick.
    It won't call onSubmit(= handleSave function) in form.

  2. 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's onClick event is no longer listened.

  3. Add key to the edit button and save button respectively.
    Inform React that these two buttons are different by adding key.
    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.

Discussion (0)