Skip to content

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

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

Closed
wmartins opened this issue Jul 26, 2024 · 1 comment

Comments

@wmartins
Copy link

wmartins commented Jul 26, 2024

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
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 (
    <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>
  )
}

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 (
    <div style={{ maxWidth: "800px", margin: "0 auto", fontFamily: "sans-serif" }}>
      <h1>Issue</h1>

      <form {...getFormProps(form)}>
        <Toggle
          field={fields.choice}
          options={options.map((option) => ({ value: option, label: option }))}
        />

        <button
          type="button"
          onClick={() => setOption("A")}
          style={{ marginRight: "3px" }}
        >
          Set option to A
        </button>
        <button
          type="button"
          onClick={() => setOption("B")}
          style={{ marginRight: "3px" }}
        >
          Set option to B
        </button>
        <button
          type="button"
          onClick={() => setOption("C")}
          style={{ marginRight: "3px" }}
        >
          Set option to C
        </button>

        <hr />

        <button type="submit">Submit</button>
      </form>
    </div>
  )
}

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);
  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

Screen.Recording.2024-07-26.at.18.24.25.mov

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
Copy link
Owner

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.

Repository owner locked and limited conversation to collaborators Aug 12, 2024
@edmundhung edmundhung converted this issue into discussion #737 Aug 12, 2024

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants