Skip to content

Conversation

@nvignola
Copy link

@nvignola nvignola commented Sep 16, 2025

Summary

Fixes an infinite loop bug in SelectItem when the id prop is explicitly passed as undefined.
The issue occurred because useId(props.id) was called after id had been destructured and removed from props, causing props.id to be undefined.

Changes

  • Fix: Changed useId(props.id) to useId(providedId) to use the originally destructured value
  • Test: Added regression test to prevent future occurrences

Test

  • All existing select tests pass
  • New regression test verifies SelectItems render with valid IDs

Closes #4593

Summary by CodeRabbit

  • Bug Fixes

    • Prevents an infinite loop when a select item's id is undefined, ensuring stable combobox/listbox behavior and correct option selection updates.
  • Tests

    • Adds a regression test confirming select items with undefined id no longer cause loops and selection updates work as expected.
  • Chores

    • Adds a patch-level changeset entry for release.

Copilot AI review requested due to automatic review settings September 16, 2025 16:44
@changeset-bot
Copy link

changeset-bot bot commented Sep 16, 2025

🦋 Changeset detected

Latest commit: 4a9bd0b

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
@ariakit/react-core Patch
@ariakit/react Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai
Copy link

coderabbitai bot commented Sep 16, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

Patch fixes SelectItem id handling so an explicit id={undefined} no longer overrides a generated id, preventing an infinite render loop. Adds a regression test targeting the id=undefined case and a changeset entry for @ariakit/react-core.

Changes

Cohort / File(s) Summary
Release notes (changeset)
.changeset/4985-fix-select-item-id-undefined.md
Adds a patch-level changeset for @ariakit/react-core describing the SelectItem infinite loop fix when id is undefined.
Core fix: SelectItem id handling
packages/ariakit-react-core/src/select/select-item.tsx
Extracts id from incoming props as providedId, computes id = useId(providedId), removes original id from forwarded props, and merges the stable id into final props to prevent an undefined prop from overriding the generated id.
Regression test
examples/select/test.ts
Adds test "SelectItem with undefined id prop does not cause infinite loop": asserts items have valid ids (not "undefined"), selecting an item updates the combobox and closes the listbox (regression for #4593).

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant U as User
  participant S as Select
  participant SI as SelectItem
  participant ID as useId()
  participant DOM as Listbox/DOM

  Note over SI,ID: New flow — ignore incoming id if explicitly undefined
  U->>S: Open select
  S->>SI: Render items (props may include id=undefined)
  SI->>ID: id = useId(providedId)
  ID-->>SI: stable id (generated or provided)
  SI->>DOM: Render item with stable id (forwarded props exclude original id)
  U->>SI: Click item
  SI-->>S: Notify selection
  S->>DOM: Update combobox and close listbox
Loading
sequenceDiagram
  autonumber
  participant S as Select
  participant SI as SelectItem (Old)
  participant ID as useId()
  participant DOM as Listbox/DOM

  Note over SI,DOM: Old flow — incoming undefined id could override generated id
  S->>SI: Render items (props include id=undefined)
  SI->>ID: id = useId(undefined)
  ID-->>SI: generated id
  SI->>DOM: Merge props (props.id === undefined overrides generated id)
  Note right of DOM: Mismatched id -> blur/focus churn
  DOM-->>SI: Repeated focus/blur -> re-renders (loop)
Loading

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

A nibble of ids, a hop through the glade,
Undefined no longer makes my code afraid.
Apples and grapes each wear their own name,
No looping burrows, no stack overflow flame.
I twitch my whiskers—selects behave! 🐇✨

Pre-merge checks and finishing touches

✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title succinctly and accurately summarizes the primary change: fixing SelectItem's infinite loop when the id prop is undefined; it is specific, concise, and directly matches the PR objectives and modified files.
Linked Issues Check ✅ Passed The changes implement the linked-issue solution by extracting the original id (providedId), calling useId(providedId) to generate a stable id, removing id from forwarded props so undefined cannot override the generated id, and adding a regression test that verifies SelectItems render valid IDs; this directly addresses the behavior and reproduction described in issue #4593.
Out of Scope Changes Check ✅ Passed All modifications are limited to packages/ariakit-react-core/src/select/select-item.tsx, the example test, and a changeset entry, with no public API signature changes or edits to unrelated packages or files, so no out-of-scope changes were detected.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 53fc6c2 and 4a9bd0b.

📒 Files selected for processing (3)
  • .changeset/4985-fix-select-item-id-undefined.md (1 hunks)
  • examples/select/test.ts (1 hunks)
  • packages/ariakit-react-core/src/select/select-item.tsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • .changeset/4985-fix-select-item-id-undefined.md
  • examples/select/test.ts
  • packages/ariakit-react-core/src/select/select-item.tsx

Tip

👮 Agentic pre-merge checks are now available in preview!

Pro plan users can now enable pre-merge checks in their settings to enforce checklists before merging PRs.

  • Built-in checks – Quickly apply ready-made checks to enforce title conventions, require pull request descriptions that follow templates, validate linked issues for compliance, and more.
  • Custom agentic checks – Define your own rules using CodeRabbit’s advanced agentic capabilities to enforce organization-specific policies and workflows. For example, you can instruct CodeRabbit’s agent to verify that API documentation is updated whenever API schema files are modified in a PR. Note: Upto 5 custom checks are currently allowed during the preview period. Pricing for this feature will be announced in a few weeks.

Please see the documentation for more information.

Example:

reviews:
  pre_merge_checks:
    custom_checks:
      - name: "Undocumented Breaking Changes"
        mode: "warning"
        instructions: |
          Pass/fail criteria: All breaking changes to public APIs, CLI flags, environment variables, configuration keys, database schemas, or HTTP/GraphQL endpoints must be documented in the "Breaking Change" section of the PR description and in CHANGELOG.md. Exclude purely internal or private changes (e.g., code not exported from package entry points or explicitly marked as internal).

Please share your feedback with us on this Discord post.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR fixes an infinite loop bug in the SelectItem component that occurred when the id prop was explicitly passed as undefined. The issue was caused by calling useId(props.id) after the id had already been destructured and removed from props.

  • Fixed the infinite loop by destructuring the id prop separately and using the original value with useId
  • Added a regression test to ensure the bug doesn't reoccur

Reviewed Changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

File Description
packages/ariakit-react-core/src/select/select-item.tsx Refactored prop handling to avoid passing undefined to useId hook
examples/select/test.ts Added regression test to verify SelectItems render with valid IDs
.changeset/4983-fix-select-item-id-undefined.md Added changeset documenting the bug fix

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

Comment on lines +81 to 87
props = { ...restPropsWithoutId };

const id = useId(providedId);
const disabled = disabledFromProps(props);
Copy link

Copilot AI Sep 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mutating the props parameter is an anti-pattern that can make code harder to understand and debug. Consider using restPropsWithoutId directly in subsequent code instead of reassigning props.

Suggested change
props = { ...restPropsWithoutId };
const id = useId(providedId);
const disabled = disabledFromProps(props);
const selectItemProps = { ...restPropsWithoutId };
const id = useId(providedId);
const disabled = disabledFromProps(selectItemProps);

Copilot uses AI. Check for mistakes.
@nvignola nvignola force-pushed the fix/select-item-id-undefined branch 2 times, most recently from be4b8b4 to 5de4e7e Compare September 16, 2025 16:45
@codesandbox-ci
Copy link

codesandbox-ci bot commented Sep 16, 2025

This pull request is automatically built and testable in CodeSandbox.

To see build info of the built libraries, click here or the icon next to each commit SHA.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (1)
packages/ariakit-react-core/src/select/select-item.tsx (1)

80-82: Avoid reassigning the props parameter.

Reassigning function parameters hurts readability and tooling. Prefer introducing a new local (e.g., let nextProps = { ...restPropsWithoutId };) and use that onward. This mirrors prior feedback.

🧹 Nitpick comments (3)
packages/ariakit-react-core/src/select/select-item.tsx (2)

77-84: Make the computed id win even if a later step re‑adds id.

To be belt-and-suspenders, set id after the spread where the element props are composed so it can’t be overridden if any wrapper adds id back.

-    props = {
-      id,
-      role: getPopupItemRole(listElement),
-      "aria-selected": selected,
-      children: value,
-      ...props,
-      autoFocus: props.autoFocus ?? autoFocus,
-      onClick,
-    };
+    props = {
+      role: getPopupItemRole(listElement),
+      "aria-selected": selected,
+      children: value,
+      ...props,
+      id, // ensure our computed id always wins
+      autoFocus: props.autoFocus ?? autoFocus,
+      onClick,
+    };

Please confirm no other hooks in this pipeline inject an id before this point. If they do, the diff above will make the behavior robust.


77-80: Comment slightly misattributes the root cause.

The loop was caused by the merged props’ id (undefined) overriding the generated one, not by passing undefined to useId itself. Consider rewording to avoid confusion for future readers.

examples/select/test.ts (1)

215-217: Tighten assertions (and drop ?.).

q.option() shouldn’t be nullable; optional chaining is unnecessary. You can also rely on jest-dom helpers you already use above.

-  expect(q.option("Apple")?.getAttribute("id")).toBeTruthy();
-  expect(q.option("Apple")?.getAttribute("id")).not.toBe("undefined");
+  expect(q.option("Apple")).toHaveAttribute("id");
+  expect(q.option("Apple").getAttribute("id")).not.toBe("undefined");
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e51983c and 5de4e7e.

📒 Files selected for processing (3)
  • .changeset/4985-fix-select-item-id-undefined.md (1 hunks)
  • examples/select/test.ts (1 hunks)
  • packages/ariakit-react-core/src/select/select-item.tsx (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
examples/select/test.ts (3)
examples/test-utils.ts (1)
  • test (26-42)
packages/ariakit-test/src/click.ts (1)
  • click (128-169)
packages/ariakit-test/src/query.ts (1)
  • q (165-165)
packages/ariakit-react-core/src/select/select-item.tsx (1)
packages/ariakit-react-core/src/utils/hooks.ts (1)
  • useId (165-178)
🔇 Additional comments (3)
.changeset/4985-fix-select-item-id-undefined.md (1)

1-5: Changelog entry is correct and scoped.

Patch note clearly describes the fix for @ariakit/react-core. LGTM.

packages/ariakit-react-core/src/select/select-item.tsx (1)

77-84: Correctly sources a stable id; fix looks good.

Using const { id: providedId, ...restPropsWithoutId } = props; and useId(providedId) prevents an undefined prop from overriding the generated id. Nice.

examples/select/test.ts (1)

203-221: Doesn't exercise id={undefined} — add an explicit example or parameterize the test.

Repo search finds no <SelectItem ... id=> usages; examples/select/test.ts only asserts generated IDs and never renders . Add a tiny example (or update this test) that renders SelectItem with id={undefined} to guard issue #4593.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (2)
packages/ariakit-react-core/src/select/select-item.tsx (2)

159-167: Make generated id authoritative in the final merge.

Place id after ...props so no intermediate transforms can override it in the rare event an upstream wrapper injects an id.

-    props = {
-      id,
-      role: getPopupItemRole(listElement),
-      "aria-selected": selected,
-      children: value,
-      ...props,
-      autoFocus: props.autoFocus ?? autoFocus,
-      onClick,
-    };
+    props = {
+      role: getPopupItemRole(listElement),
+      "aria-selected": selected,
+      children: value,
+      ...props,
+      autoFocus: props.autoFocus ?? autoFocus,
+      onClick,
+      id,
+    };

77-84: Drop redundant spread when reassigning props.

Replace props = { ...restPropsWithoutId } with props = restPropsWithoutId — same semantics, avoids an unnecessary shallow copy.

File: packages/ariakit-react-core/src/select/select-item.tsx (around lines 77–84)

Apply:

-    const { id: providedId, ...restPropsWithoutId } = props;
-    props = { ...restPropsWithoutId };
+    const { id: providedId, ...restPropsWithoutId } = props;
+    props = restPropsWithoutId;
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5de4e7e and 53fc6c2.

📒 Files selected for processing (3)
  • .changeset/4985-fix-select-item-id-undefined.md (1 hunks)
  • examples/select/test.ts (1 hunks)
  • packages/ariakit-react-core/src/select/select-item.tsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • examples/select/test.ts
  • .changeset/4985-fix-select-item-id-undefined.md
🧰 Additional context used
🧬 Code graph analysis (1)
packages/ariakit-react-core/src/select/select-item.tsx (1)
packages/ariakit-react-core/src/utils/hooks.ts (1)
  • useId (165-178)

@nvignola nvignola force-pushed the fix/select-item-id-undefined branch from 53fc6c2 to 4a9bd0b Compare September 17, 2025 09:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

SelectItem generates infinite loop issue when id prop is passed as undefined

1 participant