Skip to content

Conversation

@kawikabader
Copy link
Contributor

Summary

Introduces @bento/use-collection, a utility package providing shared hooks for collection primitives (ListBox, Menu, etc).

Why this change is needed: Collection components were duplicating patterns for React Aria integration, state management, and SSR-safe ref handling. Centralizing these patterns eliminates duplication and ensures consistency.

Impact: Reduces maintenance burden, prevents implementation inconsistencies, and provides reusable utilities for current and future collection primitives.

Changes Made

  • Added useSafeObjectRef<T> - SSR-safe ref wrapper for React Aria hooks (handles Vitest browser mode frozen objects)
  • Added useCollectionState<TState> - Rules-of-hooks compliant state hook with context fallback
  • Added renderWithOptionalContext<TState> - Helper for standalone vs composed usage patterns
  • Added useKeyboardDelegate - Memoized keyboard delegate factory delegating to React Aria
  • Comprehensive README with usage examples and React Aria source links
  • Full TypeScript support with dual ESM/CJS exports
  • Test suite covering node and browser environments

@kawikabader kawikabader requested a review from a team as a code owner December 15, 2025 22:52
@changeset-bot
Copy link

changeset-bot bot commented Dec 15, 2025

⚠️ No Changeset found

Latest commit: ac472b0

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

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

- useSafeObjectRef: SSR-safe ref handling (fixes Vitest browser mode frozen refs)
- useCollectionState: rules-of-hooks compliant state management
- renderWithOptionalContext: generic context wrapper for standalone/composed usage
- useKeyboardDelegate: memoized keyboard delegate with explicit deps

This package extracts shared utilities from @bento/listbox and @bento/menu,
providing a common foundation for collection-based primitives.
}
```

### `renderWithOptionalContext<TState>(content, state, contextState, StateContext)`
Copy link
Contributor

Choose a reason for hiding this comment

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

does this really belong in a utility called use-collection, it doesn't seem to have anything to do with that specific functionality, and IMHO would be better suited as standalone.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good point on naming clarity. While renderWithOptionalContext looks generic, it's actually solving a collection-specific pattern: ListBox and Menu both need to work both standalone AND composed within Select/Menu etc.

The dual-mode behavior (check for context, wrap if missing) is unique to collection primitives. Generic components typically don't have this requirement.

That said, I'm open to two alternatives if you feel strongly:

  1. Rename to renderCollectionWithContext to make the use case explicit
  2. Move to separate package if we anticipate non-collection components needing this pattern

What's your preference?

Copy link
Contributor

Choose a reason for hiding this comment

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

Going to leave that up to the team decide, just wanted to call this out specifically as it doesn't really fit in a hook package.

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm just not sure if this abstraction belongs in a hook outside of the current usage. Also, there seems to be a lot of exports in use-collection....that seem loosely associated with interacting with collections.

@kawikabader kawikabader self-assigned this Dec 16, 2025
@kbader-godaddy kbader-godaddy requested a review from a team December 18, 2025 16:39
@ddamato-godaddy
Copy link
Contributor

From an organizational POV, I'd worry about us identifying a pattern too quickly here. I think it's likely we find this is not used as often as we think or that we need to make specific changes for different compositions. I'm under the impression that it would be better to have the separate imports for now opposed to a single collect project.

import { forwardRef } from 'react';

const MyComponent = forwardRef<HTMLDivElement, Props>((props, ref) => {
const safeRef = useSafeObjectRef(ref);
Copy link
Contributor

Choose a reason for hiding this comment

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

}
```

### `useKeyboardDelegate(config): ListKeyboardDelegate<unknown>`
Copy link
Contributor

Choose a reason for hiding this comment

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

I could be wrong, but isn't the KeyboardDelegate used specifically for "search/typeahead" and not for navigating collections?

https://github.com/adobe/react-spectrum/blob/main/packages/%40react-aria/select/src/useSelect.ts#L84-L87


Generic state hook for collection components. Always calls the state hook unconditionally (rules-of-hooks compliance), then chooses context state if available.

**Fixes rules-of-hooks violation**: The previous pattern `contextState ?? useListState(props)` conditionally called hooks based on `contextState`, which violates React's rules. This hook always calls `useStateHook` unconditionally.
Copy link
Contributor

Choose a reason for hiding this comment

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

Is the primary use of the hook to just avoid contextState ?? useListState(props) and defining the undefined props for children and items?

}
```

### `renderWithOptionalContext<TState>(content, state, contextState, StateContext)`
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm just not sure if this abstraction belongs in a hook outside of the current usage. Also, there seems to be a lot of exports in use-collection....that seem loosely associated with interacting with collections.

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.

7 participants