3

I have a group of radio buttons being built up from some external data, as per the examples below. They render and work as I'd expect with the exception of the aria-checked attribute. Clicking a radio button toggles the checked value, but it remains true when another has been selected.

If I click each radio button in sequence, I end up with a list of radio buttons that display aria-checked="true" which obviously isn't the best experience.

I'm not sure how to go about toggling the radio buttons checked value back to false when another has been checked.

Any help would be great!

Input:

    const [isChecked, setIsChecked] = useState(false);

    <Input
        type={type}
        id={id}
        value={id}
        name={name}
        aria-label={label}
        aria-checked={isChecked}
        onChange={() => setIsChecked(!isChecked)}
    />
    <Label htmlFor={id}>{label}</Label>

Usage:

    <fieldset>
        {someData.map(item => {
            return (
                <RadioButton
                    type="radio"
                    key={item.id}
                    id={item.id}
                    name={item.name}
                    label={item.label}
                />
            );
        })}
    </fieldset>
0

2 Answers 2

4

You could try something like this.

const Radio = React.memo(function Radio({
  item,
  checked,
  onChange
}) {
  console.log("rendering", item.label);
  return (
    <label>
      {item.label}
      <input
        type="radio"
        value={item.id}
        checked={checked}
        onChange={onChange}
      />
    </label>
  );
});
const RadioList = ({ items, value, onChange }) => (
  <div>
    {items.map(item => (
      <Radio
        item={item}
        key={item.id}
        checked={item.id === value}
        onChange={onChange}
      />
    ))}
  </div>
);

const App = ({ items }) => {
  const [value, setValue] = React.useState(items[0].id);
  const [v, setV] = React.useState(items[1].id);
  const onChange = React.useCallback(
    e => setValue(e.target.value),
    []
  );
  const onOther = React.useCallback(
    e => setV(e.target.value),
    []
  );
  return (
    <div>
      <RadioList
        items={items}
        value={value}
        onChange={onChange}
      />
      <RadioList
        items={items}
        value={v}
        onChange={onOther}
      />
    </div>
  );
};
//render app
ReactDOM.render(
  <App
    items={[
      { id: "1", label: "one" },
      { id: "2", label: "two" },
      { id: "3", label: "three" },
      { id: "4", label: "four" }
    ]}
  />,
  document.getElementById("root")
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>

Sign up to request clarification or add additional context in comments.

1 Comment

This looks exactly like what I'm after! Thank you!
1

There are two things you should change here.

  1. When you are rendering a list of radio buttons under the same group, then the name attribute should be same for all the radio buttons. So instead of reading the name attribute from the someData, pass the same name attribute to each button. Or keep the same name in someData for each object.

  2. After that, I don't think the state should be on input component because once you checked the radio, you are never changing its value even after clicking on some radio button. You can try it keeping the same name attribute on each Radio button.

So the solution can be to actually pass the checked attribute to the Input component depending upon which radio is actually selected. Please check the following code:

const RadioButton = ({ type, id, name, label, checked, onChange }) => {
return (
    <>
        <input
            type={type}
            id={id}
            value={id}
            name={name}
            aria-label={label}
            aria-checked={checked}
            checked={checked}
            onChange={onChange}
        />
        <label htmlFor={id}>{label}</label>
    </>
)

}

const RadioGroup = () => {
const someData = [{
    id: 1,
    name: 'name',
    label: 'name 1'
}, {
    id: 2,
    name: 'name',
    label: 'name 2'
}, {
    id: 3,
    name: 'name',
    label: 'name 3'
}];

const [checkedValue, setIsChecked] = useState(1);

return (
    <fieldset>
        {someData.map(item => {
            return (
                <RadioButton
                    type="radio"
                    key={item.id}
                    id={item.id}
                    name="radioGroup"
                    label={item.label}
                    checked={checkedValue === item.id}
                    onChange={() => setIsChecked(item.id)}
                />
            );
        })}
    </fieldset>
)

}

4 Comments

html components generating id's that need to be unique for the entire document is not a good idea. Are you sure a html label needs to have an id? Also you are passing a new reference as a value for a prop on every render, this means you'll re render every radio, including the ones that didn't change.
The for attribute of label is not needed: Alternatively, you can nest the <input> directly inside the <label>, in which case the for and id attributes are not needed because the association is implicit:. I guess that's why you never this in React documentation because they know what a component should and should not do.
triggering on Change on any Radio actually effect the other Radio also as the checked state will change. So didn't get The first point when you say: Also you are passing a new reference as a value for a prop on every render, this means you'll re render every radio, including the ones that didn't change.
If you look at the console logs in my answer you see 2 logs on change (one gets unchecked and one gets checked). There are 8 radios though. If you pass new reference as onChange then all 8 will re render. I do pass a reference to onChange in my code but I do it in a pure component and not pass it from parent.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.