Eleven components call useId() in ways that violate the Rules of Hooks. The violations fall into two patterns and affect Checkbox, CheckboxGroup, ComboBox, Modal, Radio, RadioGroup, SecretText, Select, Switch, TextInput, and Textarea.
The Violations
Pattern A — useId() inside a nested function
Ten components wrap useId() in a local arrow function and then invoke it conditionally:
const uniqueId = () => "juno-foo-" + useId()
// ...
const theId = id || uniqueId()
useId is a hook and must be called at the top level of the component, never inside a nested function. Wrapping it in an arrow function and calling that function conditionally means the hook is only called when id is falsy, violating both rules simultaneously. This works in practice only by coincidence — React's hook order tracking will break if the condition ever changes between renders.
Affected components and lines:
| Component |
Line |
Checkbox |
Checkbox.component.tsx:141 |
CheckboxGroup |
CheckboxGroup.component.tsx:86 |
Modal |
Modal.component.tsx:116 |
Radio |
Radio.component.tsx:124 |
RadioGroup |
RadioGroup.component.tsx:102 — uniqueId() is also called twice, each invocation calling useId() inside the nested function |
SecretText |
SecretText.component.tsx:119 |
Select |
Select.component.tsx:188 |
Switch |
Switch.component.tsx:233 |
TextInput |
TextInput.component.tsx:125 |
Textarea |
Textarea.component.tsx:126 |
Pattern B — useId() inside a short-circuit expression
ComboBox calls useId() at the top level but inside a || expression:
// ComboBox.component.tsx:213
const theId = id || "juno-combobox-" + useId()
JavaScript evaluates the right-hand side of || only when the left side is falsy, so useId() is conditionally called depending on whether id was passed. This is a conditional hook call.
The Fix
For all Pattern A cases, remove the wrapper function and call useId() directly at the top level:
// Before
const uniqueId = () => "juno-foo-" + useId()
const theId = id || uniqueId()
// After
const generatedId = useId()
const theId = id || "juno-foo-" + generatedId
For RadioGroup, where uniqueId() is called twice to generate both a name and an id, replace with two separate top-level useId() calls:
// Before
const uniqueId = () => "juno-radiogroup-" + useId()
const groupName = name || uniqueId()
const groupId = id || uniqueId()
// After
const generatedName = useId()
const generatedId = useId()
const groupName = name || "juno-radiogroup-" + generatedName
const groupId = id || "juno-radiogroup-" + generatedId
For the Pattern B case in ComboBox, the same approach applies:
// Before
const theId = id || "juno-combobox-" + useId()
// After
const generatedId = useId()
const theId = id || "juno-combobox-" + generatedId
Sub-tasks
Related Issues
Eleven components call
useId()in ways that violate the Rules of Hooks. The violations fall into two patterns and affectCheckbox,CheckboxGroup,ComboBox,Modal,Radio,RadioGroup,SecretText,Select,Switch,TextInput, andTextarea.The Violations
Pattern A —
useId()inside a nested functionTen components wrap
useId()in a local arrow function and then invoke it conditionally:useIdis a hook and must be called at the top level of the component, never inside a nested function. Wrapping it in an arrow function and calling that function conditionally means the hook is only called whenidis falsy, violating both rules simultaneously. This works in practice only by coincidence — React's hook order tracking will break if the condition ever changes between renders.Affected components and lines:
CheckboxCheckbox.component.tsx:141CheckboxGroupCheckboxGroup.component.tsx:86ModalModal.component.tsx:116RadioRadio.component.tsx:124RadioGroupRadioGroup.component.tsx:102—uniqueId()is also called twice, each invocation callinguseId()inside the nested functionSecretTextSecretText.component.tsx:119SelectSelect.component.tsx:188SwitchSwitch.component.tsx:233TextInputTextInput.component.tsx:125TextareaTextarea.component.tsx:126Pattern B —
useId()inside a short-circuit expressionComboBoxcallsuseId()at the top level but inside a||expression:JavaScript evaluates the right-hand side of
||only when the left side is falsy, souseId()is conditionally called depending on whetheridwas passed. This is a conditional hook call.The Fix
For all Pattern A cases, remove the wrapper function and call
useId()directly at the top level:For
RadioGroup, whereuniqueId()is called twice to generate both a name and an id, replace with two separate top-leveluseId()calls:For the Pattern B case in
ComboBox, the same approach applies:Sub-tasks
Checkbox: fixuseIdviolationCheckboxGroup: fixuseIdviolationComboBox: fix conditionaluseIdcallModal: fixuseIdviolationRadio: fixuseIdviolationRadioGroup: fixuseIdviolation — replace single wrapped call with two top-level callsSecretText: fixuseIdviolationSelect: fixuseIdviolationSwitch: fixuseIdviolationTextInput: fixuseIdviolationTextarea: fixuseIdviolationRelated Issues