docs/react-v9/contributing/rfcs/react-components/convergence/native-element-props.md
refAuthors:
@ecraig12345, @sopranopillow, @behowell
Special thank you to everyone who contributed in the design and discussion around this RFC, including:
@bsunderhus, @layershifter, @miroslavstastny, @ling1726, @dzearing, @GeoffCoxMSFT
For certain components, it's necessary to apply top-level native props to an element besides the actual DOM root. This RFC proposes a mechanism for handling this.
This functionality should be used very sparingly to avoid confusion. The most obvious case is for input components (options for exact criteria discussed below).
For most components, it makes sense to apply native props (and ref) to the root element. Currently the root slot always gets native element props passed to the component, and is not explicitly exposed via the API.
The input components are a little unique because the actual <input> cannot be the root element of the component, since we need various wrappers for styling (and <input> can't have children). However, users may expect that some or all native props passed to the root of the component would be applied to the <input> itself. This may also be a requirement for certain 3rd-party form libraries.
For example, rendering a <Checkbox/> actually does this (before the RFC is implemented):
<slots.root>
<slots.checkmark />
<slots.input>
{children /*label*/}
</slots.root>
Or an <Input/> is roughly like this (again pre-RFC):
<slots.root>
<slots.bookendBefore />
<slots.inputWrapper>
<slots.insideStart />
<slots.input />
<slots.insideEnd />
</slots.inputWrapper>
<slots.bookendAfter />
</slots.root>
So to specify a value, you'd have to use slot props:
<Input input={{ id: 'foo', value: 'stuff' }} />
The question of where to apply the top-level ref has similar considerations and should likely follow the same pattern as native props.
The chosen solution needs to adhere to the following principles (thanks @levithomason):
To the degree possible, we'd like inputs to work nicely with 3rd-party form libraries, which may have APIs that expect to be able to pass native props to the component and have them applied to the actual <input>. (needs more research)
.value on the input, which presents its own challenges. (Someone brought this up for v8 here.)Third-party styling libraries can assume that className applies to the root DOM element. Even if we forward other props to an inner element, we should ensure that className is always applied to the root.
For reference, this proposal was labeled Option D in discussion on PR #18983.
The primary slot is the slot that receives the native props that are specified as props of the component itself.
By default, root (the outermost DOM element) is the primary slot.
root gets all of the native props specified as props on the component.root.In special cases, a component can designate a different primary slot.
className and style.className and style props are always forwarded to the root slot.root and the primary slot are exposed as props, to allow for props to be explicitly set on those slots.See Usage examples below for examples of how this works.
To avoid confusion from a user standpoint, if we're going to allow varying where top-level native props go, we need a very clear/coherent definition of where people can expect this different behavior, and/or categories of components it applies to.
role, the primary slot is the one with the component's primary role.root element as the default (this should apply to most components).Examples include:
Input, Checkbox, Dropdown, etc. These wrap the actual <input> or <select> element with styling/layout elements, but the <input> or <select> should be the primary slot.The ComponentProps type has a template parameter to specify the primary slot:
export type ComponentProps<Shorthands, Primary extends keyof Shorthands = 'root'> =
// Put the primary slot's props directly on the ComponentProps
Shorthands[Primary] & {
// Add shorthand props for the other slots
[Key in Exclude<
keyof Shorthands,
// Exclude `root` only if it is the primary slot
Primary extends 'root' ? Primary : never
>]?: ShorthandProps<NonNullable<Shorthands[Key]>>;
};
Add a function similar to getNativeElementProps function, except it handles splitting the className and style props to the root slot, as well as mixing in the primary slot.
Something along the lines of this (names and API is still a work in progress):
const [rootProps, inputProps] = getRootAndPrimaryNativeElementProps(props, 'input');
const state = {
// ...
root: rootProps,
input: inputProps, // primary slot
};
className and style always go to the root slot<Input className="foo" root={{ className: 'bar' }} /> => root element has className="bar"<Input id="foo" input={{ id: 'bar' }} /> => input has id="bar"<input> elements are always the primary slotroot as the primary slot (default case)Button falls into the default case, where root is its primary slot. All native props specified on the component go to the root slot:
Given JSX:
<Button id="myId" className="myClass" />
Resulting DOM (simplified):
<button id="myId" class="myClass" />
There is no root prop:
<Button root={{ id: 'myId' }} /> // ❌ Fails to compile: root is not a prop
input as the primary slotCheckbox specifies its input slot as the primary slot. (Its root slot is a wrapper <div> around the <input> element.)
Given JSX:
<Checkbox id="myId" className="myClass" />
Resulting DOM (simplified):
<div class="myClass">
<input id="myId" />
</div>
If props are specified on the root or input slots, they always go to the specified slot, and always win over props specified on the element itself:
Given JSX:
<Checkbox
id="myId" // ⚠ overridden by "inputId" below
className="myClass" // ⚠ overridden by "rootClass" below
style={{ color: 'red' }}
root={{ id: 'rootId', className: 'rootClass' }}
input={{ id: 'inputId', className: 'inputClass' }}
/>
Resulting DOM (simplified):
<div id="rootId" class="rootClass" style="color: red">
<input id="inputId" class="inputClass" />
</div>
There have been a number of proposals discussed, including on a previous RFC #18804, as well as in earlier iterations of this proposal.
The names of the discarded options reflect their name when they were proposed.
root (formerly the proposed solution)Allow setting a different "primary" slot (open to suggestions for the name) where top-level native props and ref are applied. This would default to root but could be customized (need to work with @bsunderhus to determine implementation approach).
To facilitate passing props to the actual root element when it's not the "primary" slot, explicitly expose the root slot in props (possibly with type constraints to prevent passing problematic things). This would be done in all components for consistency.
In the interest of consistency/clarity, customizing the "primary" slot would be discouraged unless a component falls under certain special categories: definitely input components (Input, Checkbox, etc), and see "Open Issues" below for discussion of other possibilities.
Suppose Checkbox sets its input slot as "primary." If the user does this:
<Checkbox name="foo" checked root={{ id: 'bar' }} ref={ref}>
sample
</Checkbox>
Output HTML is roughly like this (a few things omitted for clarity):
<div id="bar">
<!-- ref points to this element -->
<input type="checkbox" name="foo" checked />
<label>sample</label>
</div>
If someone really wanted to, they could still pass props explicitly on the input slot rather than top-level. This would give the same output HTML as before:
<Checkbox input={{ name: 'foo', checked: true, ref: ref }} root={{ id: 'bar' }}>
sample
</Checkbox>
TODO: Need to work with @bsunderhus to figure out a non-mergeProps version
className and style always be applied to the root?In a discussion about this topic, it was proposed that since className and style are commonly used for layout, it would be most intuitive for users if the top-level className and style are applied to the actual root DOM element (even if top-level native props were applied to a different element).
For example, suppose this Checkbox is in a flex or grid CSS layout where styling must be applied to the DOM root, and CSS class foo defines this styling.
<Checkbox name="foo" checked className="foo">
sample
</Checkbox>
If we go with applying className to the root, you'd get roughly this HTML:
<div class="foo">
<label>sample</label>
<input type="checkbox" name="foo" checked />
</div>
The real question here is, how important is this scenario to users? Is that worth introducing the behavior inconsistency?
root and add wrapper slot for root DOM element (formerly discarded option 3)Use the native input element as the root slot, and use a wrapper slot (standardized name) as the actual root DOM element. This is roughly what the component would look like internally:
<slots.wrapper>
<slots.root {...slotProps.root} />
<slots.label {...slotProps.label} />
</slots.wrapper>
So doing this would give the desired HTML as shown at the top of the "Discarded Solutions" section:
<Checkbox name="foo" checked>
sample
</Checkbox>
And if you wanted to apply props to the wrapper element, you could use slot props:
<Checkbox name="foo" checked className="foo" wrapper={{ className: 'bar' }}>
sample
</Checkbox>
Which would give you roughly this:
<div class="bar">
<input type="checkbox" name="foo" checked className="foo" />
<label>sample</label>
</div>
root means (it's weird to call something "root" which isn't actually at the root). However since root is not exposed in the public API in this proposal,wrapper is already being used for slots in some components, with a different meaning (would probably need to change this)The examples below assume you're rendering a Checkbox and this is roughly your desired HTML output (a few things omitted for clarity):
<div>
<input type="checkbox" name="foo" checked />
<label>sample</label>
</div>
Reason discarded: Counterintuitive to user; possible incompatibility with 3rd-party form libraries.
Given this:
<Checkbox name="foo" checked>
sample
</Checkbox>
You get roughly this HTML, which is not useful:
<!-- checked would be applied here but not used in DOM -->
<div name="foo">
<input type="checkbox" />
<label>sample</label>
</div>
So to specify a value, you'd have to use slot props:
<Checkbox input={{ name: 'foo', checked: true }}>sample</Checkbox>
Reason discarded: No one could come up with a concrete way to determine which props should be cherry-picked that doesn't rely on intuition (which varies by person)
Given this, and an implementation which uses a list of special props (including checked and name) that are passed to the "actual" element, you'd get the desired HTML as shown at the top of the "Discarded Solutions" section.
<Checkbox name="foo" checked>
sample
</Checkbox>
input
checked and name (and a few others) go to the input, the user might expect this behavior for all props and then be surprisedclassName or id is passed to the input, what happens if they also want to give the root a className or id?<input> as a childReason discarded: Counterintuitive API; unclear where user ought to specify certain props
This was proposed in a PR comment (see following comments for more discussion).
Provide a wrapper which applies our styling and behaviors, and have the user pass the actual <input> as a child. The wrapper would clone the child and apply any behaviors directly to the input. (This is similar to Tooltip or MenuTrigger.)
<CheckboxField>
<input />
</CheckboxField>
checked/value and onChange. More details here.. This gets especially messy if/when we need to introduce state management within the control.