react-spectrum icon indicating copy to clipboard operation
react-spectrum copied to clipboard

useSlottedContext should not throw when slot is not found

Open unional opened this issue 1 week ago โ€ข 4 comments

Provide a general summary of the issue here

Referring to this code:

if (!ctx.slots[slotKey]) {
  let availableSlots = new Intl.ListFormat().format(Object.keys(ctx.slots).map(p => `"${p}"`));
  let errorMessage = slot ? `Invalid slot "${slot}".` : 'A slot prop is required.';

  throw new Error(`${errorMessage} Valid slot names are ${availableSlots}.`);
}

https://github.com/adobe/react-spectrum/blob/main/packages/react-aria-components/src/utils.tsx#L171-L176

This violates Open/Close principle, so that it hinders the ability to compose and add new slots in extended components.

Using Button as example, I want to add a clear slot and use the button to clear the text in a input field:

<InputField>
  <ClearButton>
</InputField>
export function ClearButton() {
  return <StyledButton slot="clear">X</StyledButton>
}
export const StyledButtonContext = createContext<ContextValue<...>>(null)

export const StyledButton = forwardRef(props, ref) {
  ;[props, ref] = useContextProps(props, ref, ButtonContext]
  ;[props, ref] = useContextProps(props, ref, StyledButtonContext]

  return <Button ref={ref} {...props} style={{...}}/>
}
export function InputField({ children }) {
  return (
    <StyledButtonContext value={{ slots: { clear: { onPress: ... } } }}>
      {children}
    </StyledButtonContext>
  )
}

Since the props { slot: 'clear' } is eventually passed to useSlottedContext, it throws.

Workaround this requires manually picking out slot: 'clear' in the StyledButton, breaking encapsulation and OCP.

Something like:

export const StyledButton = forwardRef(props, ref) {
  const { slot, ...rest } = props
  
  let baseButtonProps = slot === 'clear' ? rest : props

  ;[props, ref] = useContextProps(baseButtonProps, ref, ButtonContext]
  ;[props, ref] = useContextProps({ slot, ...props}, ref, StyledButtonContext]

  baseButtonProps = slot === 'clear' ? rest : props
  
  return <Button ref={ref} {...baseButtonProps} style={{...}}/>
}

You can see this is not scalable as more slots are used in the system.

๐Ÿค” Expected Behavior?

return undefined

๐Ÿ˜ฏ Current Behavior

throw error

๐Ÿ’ Possible Solution

No response

๐Ÿ”ฆ Context

See above

๐Ÿ–ฅ๏ธ Steps to Reproduce

Hopefully the example above should be clear enough.

Version

1.13.0

What browsers are you seeing the problem on?

Chrome

If other, please specify.

No response

What operating system are you using?

macos

๐Ÿงข Your Company/Team

Palo Alto Networks

๐Ÿ•ท Tracking Issue

No response

unional avatar Dec 04 '25 21:12 unional