Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Bug: React 19 <input type="radio"/> attribute name can't be set via ref #32346

Open
nmerget opened this issue Feb 10, 2025 · 4 comments
Open
Labels
Status: Unconfirmed A potential issue that we haven't yet confirmed as a bug

Comments

@nmerget
Copy link

nmerget commented Feb 10, 2025

React version: 19

Steps To Reproduce

  1. Open Stackblitz with the provided link
  2. Open DevTools to see DOM
  3. All inputs should have the name attribute
  4. Click on first or second input
  5. name is removed from input

Link to code example: stackblitz
or GitHub repo

The current behavior

Adding name via useRef like: radio1.current.name = "radio-group"; or radio2.current.setAttribute("name", "radio-group"); adds the correct DOM attribute. The' name' attribute is removed by clicking on the <input>. Other attributes like data-name are still on the element.

The expected behavior

The name shouldn't be deleted when clicking on the input. It works with React v18 and it works if I add the name directly on <input name="radio-group"/>.

@nmerget nmerget added the Status: Unconfirmed A potential issue that we haven't yet confirmed as a bug label Feb 10, 2025
@eps1lon
Copy link
Collaborator

eps1lon commented Feb 10, 2025

There are generally no guarantees for manual DOM mutations targetting elements that are managed by React. And re-render could change manually set attributes back to the React managed value.

Why do you need to manually set those DOM properties instead of letting React handle those values?

@nmerget
Copy link
Author

nmerget commented Feb 10, 2025

We build a Design System with Mitosis, which will generate multiple targets angular, vue and react.

To use the same code for every framework it was a good approach to use the DOM because that's the base for all of them. And it works for React 18 without any issue.

My current issue comes from our Tabs which will provide two ways of using it:

Composition

<DBTabs>
  <DBTabList>
    <DBTabItem>Tab 1</DBTabItem>
    <DBTabItem>Tab 2</DBTabItem>
    <DBTabItem>Tab 3</DBTabItem>
  </DBTabList>
  <DBTabPanel>Tab Panel 1</DBTabPanel>
  <DBTabPanel>Tab Panel 2</DBTabPanel>
  <DBTabPanel>Tab Panel 3</DBTabPanel>
</DBTabs>

or

Configuration

<DBTabs tabs={[
{label: "Tab 1", content:"Tab Panel 1"},
{label: "Tab 2", content:"Tab Panel 2"},
{label: "Tab 3", content:"Tab Panel 3"}
]}>

In the configuration case there wouldn't be a problem to pass name as a prop, because I control the parent which is DBTabs. But for the composition approach, I can't control it.
The user has to add the correct name to every DBTabItem and DBTabPanel to have a working component - which may lead to mistakes.

I'm wondering why it works in React 18, and why custom attributes like data-xxx are kept after a re-render? 🙈

@bcExpt1123
Copy link

Problem

  1. React does not track ref updates for re-renders
    When you use useRef, React does not trigger a re-render when the referenced DOM element changes. Your dependencies [radio1] and [radio2] in useEffect do not actually trigger re-renders because useRef does not cause updates when it changes.

  2. Some properties (like name) behave differently in React

    • Directly setting properties on the DOM element (radio1.current.name = "radio-group") might not work reliably because React manages some attributes differently.
    • setAttribute("name", "radio-group") is usually the correct way in JavaScript, but in React, this is not the recommended approach.

Solution

  1. Instead of using useRef to set the name attribute manually, define it directly in JSX:
const App = () => {

  return (
    <div>
      <label>
        <input id="radio1" type="radio" name="radio-group" data-name="radio-group" />
        Radio1
      </label>
      <label>
        <input id="radio2" type="radio" name="radio-group" data-name="radio-group" />
        Radio2
      </label>
      <label>
        <input id="radio3" type="radio" name="radio-group" data-name="radio-group" />
        Radio3
      </label>
    </div>
  );
};

export default App;
  1. Instead of using useRef, use useState:
import { useState } from "react";

const App = () => {
  const [name] = useState("radio-group");

  return (
    <div>
      <label>
        <input id="radio1" type="radio" name={name} data-name={name} />
        Radio1
      </label>
      <label>
        <input id="radio2" type="radio" name={name} data-name={name} />
        Radio2
      </label>
      <label>
        <input
          id="radio3"
          type="radio"
          name={name}
          data-name={name}
        />
        Radio3
      </label>
    </div>
  );
};

export default App;

@nmerget
Copy link
Author

nmerget commented Mar 3, 2025

I understand the concepts of React and agree, in general you should do it with a state in React.
But there is inconsistent behavior between React 18 and 19.

If the intention is that useRef + setAttribute('name', 'XXX') shouldn't work, it would be a bug in React 18. This leads to a breaking change for my components and my users updating to React 19.

Moreover, data-xxx attributes are kept between re-renders. So it is possible to maintain the same behavior for name as well.

I'm not deeply familiar with the React code, but somehow you need to keep the internal state to pass it to the DOM. Otherwise, the data-xxx attributes would be removed from the element as well. In this case, there could be a bug where the internal name of the React element is null or undefined and overwrites the name of the DOM element.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Status: Unconfirmed A potential issue that we haven't yet confirmed as a bug
Projects
None yet
Development

No branches or pull requests

3 participants