Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: make select.web consistent #57

Open
wants to merge 3 commits into
base: main
Choose a base branch
from

Conversation

bryanmylee
Copy link
Contributor

@bryanmylee bryanmylee commented Feb 1, 2025

This solves #26 in making the <Select> component consistent on web and native.

For each <Item> component, keep track of its value to label mapping on the <Root> component by passing a MutableRefObject that stores the mapping from <Root> to <Item> via context.

On render, <Item> sets the label for its value. <Root> can then call onValueChange with labelForValueRef.current[val] ?? val, allowing feature parity with the native implementation.

This approach works perfectly. Since Item must be rendered before it can be interacted with and emit events, this always ensures that labelForValueRef[val] exists before onStrValueChange is called. Furthermore, since the value and defaultValue prop requires { value: string, label: string }, the initial render also does not have any issues.

Copy link

vercel bot commented Feb 1, 2025

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
rn-primitives ✅ Ready (Inspect) Visit Preview 💬 Add feedback Feb 4, 2025 3:02pm

@bryanmylee
Copy link
Contributor Author

Added another fix to solve #58.

Copy link
Collaborator

@mrzachnugent mrzachnugent left a comment

Choose a reason for hiding this comment

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

@bryanmylee Thanks for another contribution.

@@ -225,10 +227,17 @@ const ItemContext = React.createContext<{

const Item = React.forwardRef<ItemRef, ItemProps>(
({ asChild, closeOnPress = true, label, value, children, ...props }, ref) => {
const { labelForValueRef } = useRootContext();
labelForValueRef.current[value] = label;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Unfortunately, the react docs say:

"Do not write or read ref.current during rendering, except for initialization. This makes your component’s behavior unpredictable."

https://react.dev/reference/react/useRef#caveats

Copy link
Collaborator

Choose a reason for hiding this comment

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

It's probably best to update it with a useEffect.

React.useEffect(() => {
  labelForValueRef.current[value] = label;
}, [value, label]);

Copy link
Contributor Author

@bryanmylee bryanmylee Feb 4, 2025

Choose a reason for hiding this comment

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

That's true, but I wanted to avoid the overhead of useEffect. Since the ref is only used in isolation for the parent-child context system and only read within an event handler, I don't foresee any unpredictability in the behaviour.

Copy link
Contributor Author

@bryanmylee bryanmylee Feb 4, 2025

Choose a reason for hiding this comment

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

The only possible case where this might cause issues is if the association between value and label unexpectedly changes between concurrent renders. However, we already have the implicit guarantee that value and label should always have a static association.

Maybe, to enforce the static-ness of value and label, and follow the initialization guidelines, we treat this as initialization per key?

const { labelForValueRef } = useRootContext();
if (!(value in labelForValueRef.current)) {
  labelForValueRef.current[value] = label;
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The write-ref-on-render pattern is also used for react-use's useLatest.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@mrzachnugent I've updated the PR to only set the label for each value once. While it's not initializing the entire ref once, it is effectively only initializing each item once.

For each `<Item>` component, keep track of its `value` to `label`
mapping on the `<Root>` component by passing a `MutableRefObject`
that stores the mapping from `<Root>` to `<Item>` via context.

On render, `<Item>` sets the label for its value. `<Root>` can then call
`onValueChange` with `labelForValueRef.current[val] ?? val`, allowing
feature parity with the native implementation.

This approach works perfectly. Since `Item` must be rendered before it
can be interacted with and emit events, this always ensures that
`labelForValueRef[val]` exists before `onStrValueChange` is called.
Furthermore, since the `value` and `defaultValue` prop requires `{
value: string, label: string }`, the initial render also does not have
any issues.
@bryanmylee
Copy link
Contributor Author

closeOnPress is not handled on the web version of <Select>

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.

2 participants