2

I'm modifying some existing code in React, and I'm running into unexpected behavior with dynamic buttons within a form.

There is a button that is rendered dynamically based on the state, isEditing.

  • When isEditing is false, the button will render "Edit", and it has an onClick callback that will update the state to true.
  • When isEditing is true, the button will render "Done" and instead of an onClick callback, it will have the property type=submit. In this case, clicking the button will trigger the onSubmit callback of the form that contains it. The form's onSubmit callback will update the isEditing state back to false.

The existing code has these different versions of the button set up with the conditional && operator, and it works as expected in toggling the state with each click.

However, when I tried to recreate the same setup with a ternary instead of the &&, the behavior is broken. When isEditing is false, clicking the button will trigger the onClick callback as expected, but then it immediately triggers the onSubmit callback of the form as well.

The compiled output of both of these appear like they should be identical, but for whatever reason they are behaving differently. I'm wondering what is causing this difference in behavior. Why is the ternary version submitting the form while the && version isn't?

Here is a code snippet with the relevant parts along with a link to a codepen where I recreated the issue. State updates are handled with React Hooks. Thanks

const App = () => {
    const [isEditing, setIsEditing] = useState(false)
    const handleSubmit = event => {
        event.preventDefault()
        setIsEditing(false)     
    }
    const handleClick = () => {
        setIsEditing(true)
    }

    return (
        <>
            <form onSubmit={handleSubmit}>
                {/* WORKING */}
                {isEditing && (
                    <button type="submit">Working Done</button>
                )}
                {!isEditing && (
                    <button onClick={handleClick}>Working Edit</button>
                )}

                {/* NOT WORKING */}
                {isEditing ? (
                    <button type="submit">Broken Done</button>
                ) : (
                    <button onClick={handleClick}>Broken Edit</button>
                )}
            </form>
        </>
    )
}

https://codepen.io/HoonKing123/pen/NWwgMar?editors=1010

HoonKing
  • 21
  • 1

2 Answers2

0

Thanks for asking such an interesting question as it's very confusing as well but let me try to explain what I think is happening here.

Reason

This is not happening because of && vs Ternary, but because of using a single interpolation (curly braces in JSX) for the ternary operator.

As React tries to re-use DOM elements [wherever it needs] (especially for some particular elements like buttons, especially in the same/single interpolation) to couple DOM elements with react elements.
So, React uses the same button element for each condition with the default behaviour of submitting the form when clicked.
Note that the default type for a button inside the form is submit (ref1, ref2).
Using separate interpolation with && in your example above enforces React to use distinct elements for each button. [see Possible Solutions below to understand this better]
Check out this discussion here, specifically this comment.

This issue will persist with && as well [not only for ternary] if we use a single interpolation for it; something like this

{ isEditing && (
    <button type="submit">Working (But Now Broken) Done</button>
  ) || (
    <button onClick={handleClick}>Working (But Now Broken) Edit</button>
) }

See working example here

Possible Solutions

There are a couple of ways to fix this, let's discuss them one by one.

  • It can also work with the ternary operator just use a different interpolation statement for each case [like we did for &&] (ref). Showing this solution just to strengthen our understanding of this concept.
{ isEditing ? (
    <button type="submit">Broken (But Now Working) Done</button>
  ) : null }
{ !isEditing ? (
    <button onClick={handleClick}>Broken (But Now Working) Edit</button>
) : null }
  • Use a unique key for each button as it will force React not to reuse the existing element (ref).
  • Use event.preventDefault() in handleClick function to explicitly prevent the default behavior of the button which is form submission (ref1 ,ref2).
  • Use <input type="submit"> instead of <button type="submit"> as input is a different element, it will force React not reuse the existing element (ref).

Working Code Example

All the possible solutions and explanations with reference for each one are inside this code example - https://codepen.io/MominBinShahid/pen/vYWJYXW?editors=1010

P.S. In JSX, using ternary over && is recommended, check this for in-depth understanding https://kentcdodds.com/blog/use-ternaries-rather-than-and-and-in-jsx

  • 1
    To be clear, using `&&` retains a `false` in the JSX tree returned by the component, where a ternary expression changes the elements in-place without "extra" `false`s that are flipped around. – AKX Feb 13 '22 at 14:58
0

I've had the same issue (sorry but Stack Overflow isn't letting me put this as a comment).

I found that it does also have to do with the Button being in the same location causing the next button to trigger the click event. Adding an additional dummy button removes it.That's the crux of the issue I was searching for.

The title could be conditionally rendered button clicking first button triggers replaced button.

Anthony
  • 31
  • 3