Skip to content

Conversation

@lcmohsen
Copy link
Contributor

@lcmohsen lcmohsen commented Aug 25, 2025

The SelectOption and SelectProps and Select accept a type to be set as the value type. (defaults to string).

Summary by CodeRabbit

  • New Features

    • Select supports option values beyond plain strings (e.g., numbers), enabling more flexible data selection without UI changes.
    • Improved type safety for value handling and change events.
  • Refactor

    • Internal Select API made generic to align option values, selected value, and change handlers.
  • Documentation

    • Usage guidance updated for broader value support.
  • Bug Fixes

    • Stable keys for non-string option values to prevent key collisions.

@bolt-new-by-stackblitz
Copy link

Review PR in StackBlitz Codeflow Run & review this pull request in StackBlitz Codeflow.

@coderabbitai
Copy link

coderabbitai bot commented Aug 25, 2025

Walkthrough

Select component API in packages/components/src/ui/select.tsx was made generic. Types now parameterize option values, props, and callbacks with a type T (default string). Rendering unchanged except keys use String(option.value). Package version bumped to 0.20.0 and files formatting compacted.

Changes

Cohort / File(s) Summary
Generic Select API
packages/components/src/ui/select.tsx
Made SelectOption, SelectProps, and Select generic with <T = string>. SelectOption.value is now T. SelectProps uses options: SelectOption<T>[], value: T, onValueChange: (value: T) => void. Option keys rendered as String(option.value). No UI behavior changes.
Package metadata
packages/components/package.json
Bumped version from 0.19.50.20.0. files array formatting changed to a single-line ["dist"] entry; no change to published files.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor U as User
  participant S as Select<T>
  participant CB as onValueChange(value: T)

  U->>S: Select option (value: T)
  S->>S: Update selected value (T)
  S-->>CB: Call onValueChange with T
  Note over S,CB: DOM keys use String(option.value)
Loading

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

I nibbled types by moonlit tree,
Turned strings to T with glee and spree.
Options hop in tidy rows,
Callbacks catch where value goes.
Keys now sing in stringed delight—generic trails, a cleaner sight. 🐇✨

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/make-select-options-generic

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@github-actions
Copy link
Contributor

github-actions bot commented Aug 25, 2025

📝 Storybook Preview: View Storybook

This preview will be updated automatically when you push new changes to this PR.

Note: The preview will be available after the workflow completes and the PR is approved for deployment.

@lcmohsen lcmohsen changed the title Enhance Select component: Generic SelectProps Enhance Select component value type Aug 25, 2025
@lcmohsen lcmohsen force-pushed the fix/make-select-options-generic branch from b673ab9 to eda3b38 Compare August 25, 2025 17:09
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: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/components/src/ui/select.tsx (1)

38-49: Propagate the React.Key constraint to the component signature

Align the component generic with the constrained props to prevent unsafely wide instantiation and fix key typing downstream.

Apply this diff:

-export function Select<T = string>({
+export function Select<T extends React.Key = string>({
   options,
   value,
   onValueChange,
   placeholder = 'Select an option',
   disabled = false,
   className,
   contentClassName,
   itemClassName,
   components,
   ...buttonProps
-}: SelectProps<T>) {
+}: SelectProps<T>) {
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between b673ab9 and eda3b38.

📒 Files selected for processing (2)
  • packages/components/package.json (2 hunks)
  • packages/components/src/ui/select.tsx (5 hunks)
✅ Files skipped from review due to trivial changes (1)
  • packages/components/package.json
🧰 Additional context used
📓 Path-based instructions (10)
packages/components/src/ui/**/*.tsx

📄 CodeRabbit inference engine (.cursor/rules/form-component-patterns.mdc)

packages/components/src/ui/**/*.tsx: Build on @radix-ui components as the foundation for base UI components
Follow the component composition pattern for UI components that accept form integration

Files:

  • packages/components/src/ui/select.tsx
packages/components/src/ui/*.{tsx,ts}

📄 CodeRabbit inference engine (.cursor/rules/form-component-patterns.mdc)

Base UI components should be named as ComponentName in ui/ directory

Files:

  • packages/components/src/ui/select.tsx
**/*.{tsx,ts}

📄 CodeRabbit inference engine (.cursor/rules/form-component-patterns.mdc)

**/*.{tsx,ts}: Props interfaces should be named as ComponentNameProps
Form schemas should be named formSchema or componentNameSchema

Files:

  • packages/components/src/ui/select.tsx
packages/components/src/{remix-hook-form,ui}/*.{tsx,ts}

📄 CodeRabbit inference engine (.cursor/rules/form-component-patterns.mdc)

Always export both the component and its props type

Files:

  • packages/components/src/ui/select.tsx
{apps,packages}/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/monorepo-organization.mdc)

{apps,packages}/**/*.{ts,tsx}: Use package name imports for published packages (e.g., import { TextField } from '@lambdacurry/forms/remix-hook-form')
Import from specific entry points (e.g., import { TextField } from '@lambdacurry/forms/remix-hook-form/text-field')
Do not use relative imports across packages (e.g., avoid import { TextField } from '../../packages/components/src/remix-hook-form/text-field')
Order imports: 1) external libraries, 2) internal package imports, 3) cross-package imports, 4) type-only imports (grouped separately)

Files:

  • packages/components/src/ui/select.tsx
{apps,packages}/**/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/monorepo-organization.mdc)

{apps,packages}/**/src/**/*.{ts,tsx}: Use relative imports within the same package (e.g., import { FormControl } from './form')
Use relative imports for sibling directories (e.g., import { Button } from '../ui/button')

Files:

  • packages/components/src/ui/select.tsx
packages/components/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/monorepo-organization.mdc)

packages/components/src/**/*.{ts,tsx}: Always export both component and props type (e.g., export { ComponentName }; export type { ComponentNameProps };)
Use named exports for components for better tree-shaking (e.g., export const ComponentName = ...; avoid default exports)
Avoid default exports for components
Use tree-shaking friendly exports

Files:

  • packages/components/src/ui/select.tsx
{apps,packages}/**/src/**/*.{tsx,ts}

📄 CodeRabbit inference engine (.cursor/rules/monorepo-organization.mdc)

{apps,packages}/**/src/**/*.{tsx,ts}: Use kebab-case for component file names (e.g., text-field.tsx, data-table.tsx)
Match component name in PascalCase to the exported component from the file (e.g., text-field.tsx exports TextField)

Files:

  • packages/components/src/ui/select.tsx
packages/components/src/ui/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/monorepo-organization.mdc)

Don't import from remix-hook-form package in ui components

Files:

  • packages/components/src/ui/select.tsx
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/monorepo-organization.mdc)

Avoid circular dependencies

Files:

  • packages/components/src/ui/select.tsx
🧬 Code graph analysis (1)
packages/components/src/ui/select.tsx (1)
packages/components/src/remix-hook-form/select.tsx (2)
  • SelectProps (7-20)
  • Select (22-61)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: deploy

Comment on lines +8 to 11
export interface SelectOption<T = string> {
label: string;
value: string;
value: T;
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Constrain T to React.Key to avoid key collisions and invalid data- attribute values*

Allowing any T (including objects) introduces concrete problems:

  • React keys: String(object) collapses to "[object Object]" causing duplicate keys and unstable reconciliation.
  • Equality: o.value === value is unreliable for objects across renders.
  • DOM data-* attributes: React types expect string | number; arbitrary T may not be assignable.

Constrain T to React.Key (string | number) to keep the API flexible while safe.

Apply this diff:

-export interface SelectOption<T = string> {
+export interface SelectOption<T extends React.Key = string> {
   label: string;
-  value: T;
+  value: T;
 }

If you truly need object values, consider adding a getKey: (value: T) => React.Key and equals?: (a: T, b: T) => boolean; but constraining to React.Key is the simplest, safest path for now.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export interface SelectOption<T = string> {
label: string;
value: string;
value: T;
}
export interface SelectOption<T extends React.Key = string> {
label: string;
value: T;
}
🤖 Prompt for AI Agents
In packages/components/src/ui/select.tsx around lines 8 to 11, the SelectOption
generic should be constrained to React.Key to prevent objects from being used as
values (which breaks React keys, equality checks, and data-* typing); change the
interface generic to extend React.Key (e.g., SelectOption<T extends React.Key =
string>) and update any related component generics/usage sites to respect the
new constraint; if you must support object values later, add a getKey: (value:
T) => React.Key and optional equals: (a: T, b: T) => boolean instead of allowing
arbitrary T.

Comment on lines +25 to +29
export interface SelectProps<T = string>
extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'value' | 'onChange'> {
options: SelectOption<T>[];
value?: T;
onValueChange?: (value: T) => void;
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Mirror the React.Key constraint on SelectProps; optionally firm up controlled vs. uncontrolled API

  • Essential: Propagate the React.Key constraint so options/value/onValueChange remain type-safe and compatible with DOM attributes and key usage.
  • Optional: The component behaves as controlled-only; consider typing the props as a controlled/uncontrolled union if you intend to support local state later.

Apply this diff:

-export interface SelectProps<T = string>
+export interface SelectProps<T extends React.Key = string>
   extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'value' | 'onChange'> {
   options: SelectOption<T>[];
-  value?: T;
-  onValueChange?: (value: T) => void;
+  value?: T;
+  onValueChange?: (value: T) => void;

Optional controlled/uncontrolled typing (outside this hunk) you can adopt later:

type Controlled<T extends React.Key = string> = {
  value: T;
  onValueChange: (value: T) => void;
};
type Uncontrolled<T extends React.Key = string> = {
  defaultValue?: T;
  onValueChange?: (value: T) => void;
};
export type SelectProps<T extends React.Key = string> =
  BaseButtonProps & { options: SelectOption<T>[] } & (Controlled<T> | Uncontrolled<T>);

To ensure downstream wrappers propagate the generic, especially remix-hook-form/select.tsx, run:

I can open a follow-up PR to generify the remix wrapper if needed.


🏁 Script executed:

#!/bin/bash
# Verify remix-hook-form wrapper uses the generic UISelectProps<T> and re-exports generic SelectProps<T>
fd -a 'select\.tsx$' packages/components/src/remix-hook-form | while read -r f; do
  echo "---- $f"
  rg -n -C2 'UISelectProps<|export interface SelectProps(.*)<|export function Select(.*)<' "$f"
done

Length of output: 1387


Propagate T extends React.Key through remix-hook-form wrappers

The remix-hook-form wrappers for Select are currently stripping out the new generic from UISelectProps<T>. To maintain type-safety and consistency with the core SelectProps<T extends React.Key>, we need to generify each wrapper.

• packages/components/src/remix-hook-form/select.tsx

  • Change the exported props interface to accept T extends React.Key = string and forward it to UISelectProps<T>
  • Update the Select component signature to <T extends React.Key = string>(props: SelectProps<T>) and render <UISelect<T> … />
-export interface SelectProps extends Omit<UISelectProps, 'value' | 'onValueChange'> {
+export interface SelectProps<T extends React.Key = string>
+  extends Omit<UISelectProps<T>, 'value' | 'onValueChange'> {
   name: string;
   label?: string;
}

-export function Select(props: SelectProps) {
+export function Select<T extends React.Key = string>(props: SelectProps<T>) {
   return <UISelect {...props} />;
}

• packages/components/src/remix-hook-form/canada-province-select.tsx
• packages/components/src/remix-hook-form/us-state-select.tsx

  • Both import UISelectProps without a generic parameter and export a non-generic component.
  • Introduce <T extends React.Key = string> on the props and component, and call <UISelect<T> … />.

Example diff for one wrapper:

-import { Select as UISelect, type SelectProps as UISelectProps } from '../ui/select';
-
-export interface CanadaProvinceSelectProps
-  extends Omit<UISelectProps, 'value' | 'onValueChange'> {}
-
-export function CanadaProvinceSelect(props: CanadaProvinceSelectProps) {
+import { Select as UISelect, type SelectProps as UISelectProps } from '../ui/select';
+
+export interface CanadaProvinceSelectProps<T extends React.Key = string>
+  extends Omit<UISelectProps<T>, 'value' | 'onValueChange'> {}
+
+export function CanadaProvinceSelect<T extends React.Key = string>(
+  props: CanadaProvinceSelectProps<T>
+) {
    return <UISelect options={canadaProvinceOptions} {...props} />;
}

Essential: Apply the same generic <T extends React.Key> pattern across all remix-hook-form wrappers so they correctly propagate the generic constraint.
Optional: Later, if you’d like to support both controlled and uncontrolled usage, consider adopting the controlled/uncontrolled union pattern discussed previously.

🤖 Prompt for AI Agents
In packages/components/src/ui/select.tsx around lines 25 to 29, the SelectProps
generic is currently declared as SelectProps<T = string> but the remix-hook-form
wrappers expect T extends React.Key; update this file and the three
remix-hook-form wrappers to use the constrained generic. Specifically: change
the exported SelectProps declaration to SelectProps<T extends React.Key =
string>, then update any Select component signatures to <T extends React.Key =
string>(props: SelectProps<T>) and ensure any internal or exported UISelect
usage is parameterized as UISelect<T>. Apply the same change in
packages/components/src/remix-hook-form/select.tsx,
packages/components/src/remix-hook-form/canada-province-select.tsx, and
packages/components/src/remix-hook-form/us-state-select.tsx — make their
exported props interfaces generic as SelectProps<T extends React.Key = string>,
change the component definitions to accept the generic (<T extends React.Key =
string>(props: SelectProps<T>)), and render the core component as <UISelect<T>
... /> so the generic propagates through.

const isEnterCandidate = query.trim() !== '' && enterCandidate?.value === option.value && !isSelected;
return (
<li key={option.value} className="list-none">
<li key={String(option.value)} className="list-none">
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Use the actual value as the React key (no String cast) once T is React.Key

Casting to String masks duplicate keys for non-primitives. With T constrained to React.Key, prefer the raw value for stable, unique keys.

Apply this diff:

-                <li key={String(option.value)} className="list-none">
+                <li key={option.value} className="list-none">

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In packages/components/src/ui/select.tsx around line 178, the list item
currently uses String(option.value) as the React key which masks duplicate keys
for non-primitive values; update the component so the generic type T is
constrained to React.Key (e.g., T extends React.Key) and use the raw
option.value directly as the key (key={option.value}) instead of casting to
String, ensuring the key prop receives the actual React.Key value for stable
uniqueness.

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.

3 participants