Search code examples
javascriptreactjs

Weird behaviour with ternary switching out the same button component in React causing form to submit


When we use a ternary to swap out a button with type="button" for a button with type="submit" in a form in React, the swapped-in button causes the form to submit.

import {useState} from "react";


function App() {
    const [isClicked, setIsClicked] = useState(false);
    const handleClick = () => {
        setIsClicked(true)
    };

    return (
        <form action='/submitted'>
            <div>
                <label htmlFor="firstName">First Name</label>
                <input id="firstName" name="firstName" placeholder="John"/>
            </div>
            <div>
                <label htmlFor="lastName">Last Name</label>{' '}
                <input id="lastName" name="lastName" placeholder="Doe"/>
            </div>
            {isClicked
                ? <button type="submit">Submit</button>
                : <button type="button" onClick={handleClick}>Click Me</button>}
        </form>
    )
}

export default App

But if we split the ternary into two conditionals the form will not be submitted

import {useState} from "react";


function App() {
    const [isClicked, setIsClicked] = useState(false);
    const handleClick = () => {
        setIsClicked(true)
    };

    return (
        <form action='/submitted'>
            <div>
                <label htmlFor="firstName">First Name</label>
                <input id="firstName" name="firstName" placeholder="John"/>
            </div>
            <div>
                <label htmlFor="lastName">Last Name</label>
                <input id="lastName" name="lastName" placeholder="Doe"/>
            </div>
            {isClicked ? <button type="submit">Submit</button> : null}
            {!isClicked ? <button type="button" onClick={handleClick}>Click Me</button> : null}
        </form>
    )
}

export default App

Also, if the swapped component is not the same component type, it won't be submitted.

import {useState} from "react";

const SubmitButton = () => <button type="submit">Submit</button>;

function App() {
    const [isClicked, setIsClicked] = useState(false);
    const handleClick = () => {
        setIsClicked(true)
    };

    return (
        <form action='/submitted'>
            <div>
                <label htmlFor="firstName">First Name</label>
                <input id="firstName" name="firstName" placeholder="John"/>
            </div>
            <div>
                <label htmlFor="lastName">Last Name</label>
                <input id="lastName" name="lastName" placeholder="Doe"/>
            </div>
            {isClicked
                ? <SubmitButton/>
                : <button type="button" onClick={handleClick}>Click Me</button>}
        </form>
    )
}

export default App

React is trying to do something clever here, but can anyone explain why this happens?

I can understand that the new element is swapped in when the onClick handler is triggered. However, I don't understand why the new element is treated as having been clicked when it clearly hasn't been.


Solution

  • Looks like it has to do with React's reconciliation process and the diffing algorithm: https://legacy.reactjs.org/docs/reconciliation.html

    DOM Elements Of The Same Type 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.

    When we use a ternary, the elements are considered to be the same node, so when the elements on both sides of the ternary match, React will update the previous node in place.

    React docs state that the recommended remedy for this is to put keys on the elements, as @TinouHD stated in their answer.

    When children have keys, React uses the key to match children in the original tree with children in the subsequent tree.

    That's why this will work:

    {isClicked
        ? <button key="submit" type="submit">Submit</button>
        : <button key="button" type="button" onClick={handleClick}>Click Me</button>}
    

    The submit triggering on click is not a React-specific issue see this pure HTML and JS:

    <!DOCTYPE html>
    <html>
    
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width">
        <title>Swap button pure JS demo</title>
    </head>
    
    <body>
        <form onsubmit="alert('submitted with swapped attribute')">
            <button type="button" id="button">Click me</button>
        </form>
        <script>
            document.getElementById("button").addEventListener("click", function (e) {
                e.target.setAttribute("type", "submit");
            })
        </script>
    </body>
    
    </html>