Skip to content

[Task](ui): fix useId-related Rules of Hooks violations across form components #1589

@franzheidl

Description

@franzheidl

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:102uniqueId() 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

  • Checkbox: fix useId violation
  • CheckboxGroup: fix useId violation
  • ComboBox: fix conditional useId call
  • Modal: fix useId violation
  • Radio: fix useId violation
  • RadioGroup: fix useId violation — replace single wrapped call with two top-level calls
  • SecretText: fix useId violation
  • Select: fix useId violation
  • Switch: fix useId violation
  • TextInput: fix useId violation
  • Textarea: fix useId violation

Related Issues

Metadata

Metadata

Assignees

No one assigned

    Labels

    ui-componentsAll tasks related to juno-ui-components library

    Type

    Projects

    Status

    New

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions