docs/react-v9/contributing/rfcs/react-components/convergence/root-as-slot.md
@bsunderhus @layershifter @ling1726
This RFC proposes to treat root as a slot to simplify signature, improve Typings and avoid conflicts between root and other slots signatures.
Main changes would be:
root becomes a regular slot on component stateas prop on every slotgetNativeElementProps on every slot, will be used only for rootThere are disparities in how root is treated compared to slots. Some discussions have popped up due to these divergences between slots and root:
Typings for root aren't available as there's no way to split root from internal state.root doesMajor differences between root (shown below as 🌿) and slots (shown below as 🎰):
as prop 🌿For the user of the component perspective there will be no changes in the signature.
root to a slotThis RFC proposes to treat root as a slot.
Treating root as a slot means having to declare root Typings together with other slots. This will impact in a components state signature, where root will have to be declared.
⚠️ That doesn't mean
rootwill be available in component's properties interface
export type Slots = {
slot: ObjectShorthandProps<SlotProps>;
};
export interface Props extends ComponentProps<Slots> & React.HTMLAttributes<HTMLElement>
export interface State extends ComponentState<Slots>
// use*State hook
function useState({ slot, ...props }: Props): State {
// ...
return {
...props,
slot: resolveShorthand(slot),
};
}
With the modifications:
export type Slots = {
root: ObjectShorthandProps<React.HTMLAttributes<HTMLElement>, HTMLElement>;
slot: ObjectShorthandProps<SlotProps>;
};
export interface Props extends ComponentProps<Slots> {
/* ... */
}
export interface State extends ComponentState<Slots> {
/* ... */
}
// use*State hook
function useState({slot, ...props}: Props): State {
// ...
return {
slot: resolveShorthand(slot)
root: getNativeElementProps(state.components.root, props),
};
}
as property (but with restrictions)Cancels Do not support as prop for slots that render native DOM elements as it already conflicts with 1st rule of ARIA opt-out mechanism. This would add as property for ObjectShorthandProps signature for all slots.
To ensure as property is used for opt-out mechanism only native elements will be supported. Typings must be used to ensure which native elements are supported.
ObjectShorthandProps should be a model that shorthand properties should extend from, and to ensure as property is only provided in cases that opt-out mechanism is required the never type should be used to disable as property on default cases.
type ObjectShorthandProps<Props = {}, Ref = unknown, As extends keyof JSX.IntrinsicElements = never> = Props &
React.RefAttributes<Ref> & {
as?: As;
children?: Props['children'] | ShorthandRenderFunction<Props>;
};
The case for useARIAButton is a good example of using this signature:
export type ARIAButtonShorthandProps =
| ObjectShorthandProps<JSX.IntrinsicElements['button'], HTMLButtonElement, /*as*/ 'button'>
| ObjectShorthandProps<JSX.IntrinsicElements['div'], HTMLDivElement, /*as*/ 'div'>
| ObjectShorthandProps<JSX.IntrinsicElements['span'], HTMLSpanElement, /*as*/ 'span'>
| ObjectShorthandProps<JSX.IntrinsicElements['a'], HTMLAnchorElement, /*as*/ 'a'>;
ARIAButtonProps is a union between types from button, div, span and anchor native elements.
⚠️ using union types for declaring properties is not perfect https://catchts.com/unions
getNativeElementPropsThe problem reported on Widening Types, makes this a little bit controversial.
Although types should be enough to ensure that only supported properties will be passed to a slot, this is less true for root at least due to the Widening Types problem. Aside from root, it's safe to say that getNativeElementProps is not necessary.
Right now ref is not supported on Typings for slots. This proposes that ref should be part of the internal interface of ObjectShorthandProps to allow access of references to internal slots of a given component.
type ObjectShorthandProps<Props = {}, Ref = unknown, As extends keyof JSX.IntrinsicElements = never> = Props &
React.RefAttributes<Ref> & {
as?: As;
children?: Props['children'] | ShorthandRenderFunction<Props>;
};
Changes are located mostly on react-utilities/compose methods and Typings.
ObjectShorthandProps will have to support as and ref properties with Generics:
type ObjectShorthandProps<Props = {}, Ref = unknown, As extends keyof JSX.IntrinsicElements = never> = Props &
React.RefAttributes<Ref> & {
as?: As;
children?: Props['children'] | ShorthandRenderFunction<Props>;
};
All other Typings would be adapted for the changes provided by ObjectShorthandProps.
getSlots method would stop having special cases for root and only a simple loop around all provided slots would be enough.
export function getSlots<R extends ObjectShorthandPropsRecord>(
state: ComponentState<R>,
slotNames: (keyof R)[] = [],
): {
slots: Slots<R>;
slotProps: SlotProps<R>;
} {
const slots = {} as Slots<R>;
const slotProps = {} as SlotProps<R>;
for (const slotName of slotNames) {
const [slot, props] = getSlot(state, slotName);
slots[slotName] = slot;
slotProps[slotName] = props;
}
return { slots, slotProps: slotProps };
}
root as a simple slot simplifies TypingsgetNativeElementProps for all slots means possible performance improvementroot becomes a regular slot, it might be possible developers forget to include it in the list of shorthandsPartial or Required (this can be mitigated by forcing optional on props and required on state)Both of these could be mitigated easily enough by good documentation and possibly also lint rules or danger checks.
Since Typings for root is a subset of the interface that declares the properties of a given component, widening mechanism from assigning types will ensure that root Typings are compatible with props which is not necessarily true! This implicates in some conflicts on properties spreading through state and root slot.
The problem can be verified in the next example:
type ComponentProps<S extends ObjectShorthandPropsRecord> = Omit<
{ [Key in keyof S]?: ShorthandProps<S[Key]> },
'root'
> &
S['root']; // this will make Slots['root'] part of Props
type Slots = {
root: ObjectShorthandProps<React.HTMLAttributes<HTMLElement>, HTMLElement>;
};
interface Props extends ComponentProps<Slots> {
/**
* Property that can be passed through props
* but doesn't belong on the element itself,
* it's only used internally by state
*/
stateSpecificProperty?: unknown;
}
interface State extends ComponentState<Slots> {
stateSpecificProperty: unknown;
}
// ...
// use*State hook
return {
// This should error 🚨 because of stateSpecificProperty,
// but it doesn't due to type widening
root: resolveShorthand(props),
};
A easy solution would be to use getNativeElementProps to filter out properties in this specific case:
// use*State hook
return {
// This filters out stateSpecificProperty ✅
root: getNativeElementProps(state.components.root, props),
};