How to Bind an Array to Radio Buttons in React

Beginner8 min interview
Skills tested:
Rendering radio inputs from an array using map() with a stable keySetting checked={selectedValue === option.value} to make the group controlledUsing the name attribute to group radio buttons so only one can be selected at a timeUsing a shared onChange handler for all buttons in the groupWrapping the group in fieldset and legend for accessibility

Advertisement

🧩 Scenario

Radio button groups from arrays appear in plan selectors, gender fields, rating scales, delivery method pickers, and any single-choice option set. The pattern extends naturally from a static inline group to a dynamically populated one from an API. The key mechanics are: checked from state, name for grouping, and fieldset/legend for accessibility.

Architecture Walkthrough

Controlled Radio Group

A controlled radio group works the same as any other controlled input: the checked prop comes from state, and onChange updates the state. For a group, checked is a boolean comparison: checked={selectedValue === option.value}. Only one option can match at a time, so only one radio is checked at any render.

The name attribute is what the browser uses to group radio buttons for single-selection behavior. Without a shared name, clicking one radio does not deselect the others because the browser has no way to know they are in the same group. All radios in the same group must have the same name value.

Key and Value Considerations

Each <input type="radio" /> needs a key on its wrapper element (or on the input itself if there is no wrapper). Use the option's stable ID or value, not the array index. The value prop on the radio input is what e.target.value returns when that radio is selected. This is always a string, even if the underlying option ID is a number. When comparing in checked, ensure both sides are the same type.

Accessibility

Wrap the group in a <fieldset> with a <legend> that names the group. This is a semantic HTML requirement for radio button groups, not just a visual nicety: screen readers announce the legend text when the user focuses any radio in the group, giving the context of what they are choosing. Without it, a screen reader user would hear only the individual option labels with no group context.


Key Code Explained

// String values: simple comparison
const PLAN_OPTIONS = [
  { value: 'free', label: 'Free', price: '$0/month' },
  { value: 'pro', label: 'Pro', price: '$29/month' },
  { value: 'enterprise', label: 'Enterprise', price: '$99/month' },
] as const;

type Plan = (typeof PLAN_OPTIONS)[number]['value'];

function PlanRadioGroup({ onChange }: { onChange: (plan: Plan) => void }) {
  const [selectedPlan, setSelectedPlan] = useState<Plan>('free');

  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const value = e.target.value as Plan;
    setSelectedPlan(value);
    onChange(value);
  };

  return (
    <fieldset>
      <legend>Select a Plan</legend>

      {PLAN_OPTIONS.map((option) => (
        <label key={option.value} className="radio-label">
          <input
            type="radio"
            name="plan"             // same name groups all radios
            value={option.value}
            checked={selectedPlan === option.value}  // controlled: checked from state
            onChange={handleChange}
          />
          <span>{option.label}</span>
          <span className="price">{option.price}</span>
        </label>
      ))}
    </fieldset>
  );
}


// Object array with numeric IDs: careful type comparison
interface PaymentMethod {
  id: number;
  label: string;
  icon: string;
}

const PAYMENT_METHODS: PaymentMethod[] = [
  { id: 1, label: 'Credit Card', icon: 'CreditCard' },
  { id: 2, label: 'PayPal', icon: 'Paypal' },
  { id: 3, label: 'Bank Transfer', icon: 'Bank' },
];

function PaymentMethodSelector() {
  const [selectedId, setSelectedId] = useState<number | null>(null);

  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    // e.target.value is always a string — parse to number for comparison
    setSelectedId(parseInt(e.target.value, 10));
  };

  return (
    <fieldset>
      <legend>Payment Method</legend>

      {PAYMENT_METHODS.map((method) => (
        <label key={method.id} className="radio-label">
          <input
            type="radio"
            name="payment-method"
            value={method.id}  // serialized to string in the DOM
            // Compare number to number — both sides must be the same type
            checked={selectedId === method.id}
            onChange={handleChange}
          />
          {method.label}
        </label>
      ))}

      {selectedId && (
        <p>Selected: {PAYMENT_METHODS.find((m) => m.id === selectedId)?.label}</p>
      )}
    </fieldset>
  );
}


// Resettable radio group
function ResettableRating() {
  const RATINGS = [1, 2, 3, 4, 5];
  const [rating, setRating] = useState<number | null>(null);

  return (
    <fieldset>
      <legend>Rate Your Experience</legend>

      {RATINGS.map((r) => (
        <label key={r}>
          <input
            type="radio"
            name="rating"
            value={r}
            checked={rating === r}
            onChange={() => setRating(r)}
          />
          {r}
        </label>
      ))}

      {/* Reset: set state to null and no radio has checked=true */}
      <button type="button" onClick={() => setRating(null)}>
        Clear
      </button>
    </fieldset>
  );
}

In ResettableRating, resetting to null works because no option will satisfy checked={null === r} (always false), so all radios are unchecked. This is only possible with a controlled group — an uncontrolled radio group cannot be reset programmatically without manipulating the DOM directly.


Tradeoffs

ApproachResettableValue readable in stateAccessible with legendUse when
Controlled (checked from state)YesYesYesAll cases — always prefer controlled
Uncontrolled (no checked prop)No (without DOM access)No (only on submit)YesSubmit-only, no programmatic reset needed

What Interviewers Actually Check

  • Whether you set checked={selectedValue === option.value} from state
  • Whether you include the name attribute on every radio in the group
  • Whether you use a stable key (not index)
  • Whether you wrap in fieldset and legend for accessibility
  • Whether you know e.target.value is always a string and handle type comparison correctly

Follow-Up Questions

  1. How would you build a radio group that allows deselection (clicking the selected radio clears the selection)?
  2. How would you make individual radio options disabled while keeping others enabled?
  3. How does ARIA relate to radio button groups, and when would you use role="radiogroup" instead of <fieldset>?
  4. How would you test a radio group with React Testing Library, specifically verifying that selecting one option deselects the others?
  5. How does react-hook-form's register() handle radio button groups compared to the manual controlled approach?

Common Candidate Mistakes

  • Not setting checked from state, making the group uncontrolled and unable to reset programmatically
  • Missing the name attribute, allowing multiple radios to be checked simultaneously in the browser
  • Using array index as the key, which causes state bugs if the options array can be reordered or filtered
  • Not wrapping in <fieldset> and <legend>, which is an accessibility requirement for grouped inputs
  • Comparing checked={selectedId === option.id} when selectedId is a string from e.target.value and option.id is a number, causing the comparison to always be false

Interview Readiness Checklist

Before you leave this question, make sure you can answer:

  • Can you render a radio group from an array with checked from state?
  • Can you use the name attribute to enforce single-selection across the group?
  • Can you use a shared onChange to update the selected value for any radio in the group?
  • Can you wrap the group in fieldset and legend for accessibility?
  • Can you handle numeric IDs where e.target.value is a string, and fix the type comparison?

Summary

A controlled radio group renders each option as an <input type="radio" /> from an array with map(). The checked prop is a boolean comparison: checked={selectedValue === option.value}. Only one option matches at a time, making the group single-select. The name attribute must be the same for all radios in the group so the browser enforces single-selection.

e.target.value is always a string. When option IDs are numbers, parse the value with parseInt in onChange before storing in state, and compare number to number in checked. Comparing a number to the string from e.target.value without parsing always returns false, making no radio appear selected.

Wrap the entire group in <fieldset> with a <legend> for accessibility. Screen readers announce the legend text when focusing any radio in the group, giving users the context of what they are choosing. A controlled group can be reset by setting state to null or an empty value, at which point no radio satisfies the checked comparison and all appear unselected.

Frequently Asked Questions

Do I need a separate onChange for each radio button?

No. One onChange handler on each input in the group is enough. They all call the same setter with e.target.value. You can also put the handler on the fieldset or a wrapper div if preferred.

Advertisement


Stay Updated

Get the latest frontend challenges, interview questions and tutorials delivered to your inbox.

Advertisement