edmundhung / conform

A type-safe form validation library utilizing web fundamentals to progressively enhance HTML Forms with full support for server frameworks like Remix and Next.js.
https://conform.guide
MIT License
1.8k stars 101 forks source link

Potential bug when wrapping `<input type="radio">` in a custom component and updating it manually #722

Closed wmartins closed 1 month ago

wmartins commented 1 month ago

Describe the bug and the expected behavior

Hello there! I've been seeing an issue while using conform with <input type="radio">.

For context, I have created a custom component that wraps and hides <input type="radio"> in order to customize it visually. This component is quite simple, as it receives the available options and the field from conform:

const Toggle = ({ field, options }) => {
  return (
    <div>
      {options.map(({ value, label }) => {
        const isChecked = value === field.value;

        return (
          <label key={value} style={{ display: "block" }}>
            {label}
            <input
              autoComplete="off"
              type="radio"
              name={field.name}
              value={value}
              defaultChecked={value === field.initialValue}
            />
            {isChecked && " (Checked)"}
          </label>
        );
      })}
    </div>
  )
}

This was loosely based on the documentation available at https://conform.guide/checkbox-and-radio-group.

The problem that I was seeing with this is that, on the same form, I want to have the ability to set the value of this field externally. For example, having a button that, when clicked, sets the value of that field. For example:

Screenshot showing three radio buttons, with buttons to select each of them

What I have seen is that, calling form.update seems to update the field in conform, but it messes up with the actual DOM element.

Conform version

v1.1.5

Steps to Reproduce the Bug or Issue

Code ```js import React from 'react'; import ReactDOM from 'react-dom/client'; import { getFormProps, useForm } from "@conform-to/react"; import { parseWithZod } from "@conform-to/zod"; import { z } from "zod"; const Toggle = ({ field, options }) => { return (
{options.map(({ value, label }) => { const isChecked = value === field.value; return ( ); })}
) } const App = () => { const options = ["A", "B", "C"]; const [form, fields] = useForm({ shouldRevalidate: "onSubmit", onValidate({ formData }) { return parseWithZod(formData, { schema: z.object({ choice: z.union([z.literal("A"), z.literal("B"), z.literal("C")]), }), }); }, defaultValue: { choice: "A", }, }); const setOption = (value) => { form.update({ name: fields.choice.name, value, }); }; return (

Issue

({ value: option, label: option }))} />
) } const root = ReactDOM.createRoot(document.getElementById('root')); root.render( ); ```
  1. Run the code shared here
  2. Click in "Select option B"
  3. See that the radio that's actually selected is "A", but the visual indicator shows that "B" is selected
  4. Click in "Submit"

What browsers are you seeing the problem on?

Chrome, Firefox

Screenshots or Videos

https://github.com/user-attachments/assets/3f57b490-e5cc-4c58-b186-44fed17603b9

You can see that you can click in Set option to [option], and it visually indicates that the option is selected, but the selection doesn't change. When I submit the form, you can see that the value submitted is the initial one.

Additional context

After a while, I realized that there's one simple way to solve this issue. In the <Toggle> component that I have created, I can pass a key={field.key}, and it all works out just fine.

Change this:

const Toggle = ({ field, options }) => {
  return (
    <div>
      {options.map(({ value, label }) => {
        const isChecked = value === field.value;

To:

const Toggle = ({ field, options }) => {
  return (
    <div key={field.key}>
      {options.map(({ value, label }) => {
        const isChecked = value === field.value;

I understand why this solves the problem, but I don't know if this is a bug or me misusing the library. In any case, I thought it'd be good to write an issue to share this problem in case anyone else is facing. Maybe the solution for this would be to simply add a note in https://conform.guide/checkbox-and-radio-group.

Thank you!

edmundhung commented 1 month ago

The use of key is the suggested solution as of v1.1.5.

Conform does not modify the inputs automatically and relies on the key to re-mount the inputs with the updated initialValue. This does not limit to radio inputs, but any inputs that you wanna use form.update() to update its value. That's why all the inputs will have at least these 3 properties set:

<input key={field.key} name={field.name} defaultValue={field.initialValue} />

Having said that, this is gonna be improved soon. We will be landing the improvements on #729 in v1.2.0 which should removes the need of setting a key going forward.