冲突
冲突
在上一课的“切换”练习中,我们看到了如果存在冲突,将所有 props 传递下去可能会导致一些问题。
我们来看一个最简示例:
function Checkbox({ label, ...delegated }) {
const id = React.useId();
return (
<>
<label htmlFor={id}>{label}</label>
<input id={id} type="checkbox" {...delegated} />
</>
);
}这个 Checkbox 组件会给 <input> 应用两个硬编码属性: type 和 id 。
现在,假设该组件的使用者如下使用它:
<Checkbox
label="Do you agree to the terms?"
type="button"
onClick={handleAgreeToTerms}
/>type 和 onClick 属性并未在 Checkbox 组件中指定,因此它们会被收集到 delegated 对象中,并被应用到 <input> 上:
// Here's the React element that will be created:
<input id={id} type="checkbox" type="button" onClick={handleAgreeToTerms} />我们为 type 指定了两个不同的值,在遇到此类冲突时,后面的值会覆盖前面的值。因此,这个输入框最终会是一个按钮而不是复选框。
本质上,使用者“黑”了我们的 Checkbox 组件,让它不再渲染成复选框!
让我们重写 Checkbox 组件,优先展开提供的 props:
function Checkbox({ label, ...delegated }) {
const id = React.useId();
return (
<>
<label htmlFor={id}>{label}</label>
<input
// {!code highlight}
{...delegated}
// {!code highlight}
id={id}
// {!code highlight}
type="checkbox"
/>
</>
);
}通过这个更改,相同的 <Checkbox> 元素会产生不同的结果:
<input
// Delegated props:
type="button"
onClick={handleAgreeToTerms}
// Built-in attributes:
id={id}
type="checkbox"
/>
// After removing the duplicate `type`, we're left with:
<input
onClick={handleAgreeToTerms}
id={id}
type="checkbox"
/>因为我们调换了顺序,用户提供的 type="button" 现在会被内置的 type="checkbox" 覆盖。
在编写 React 组件时,我们可以决定赋予使用者多大的控制权。我们可以选择允许他们覆盖哪些属性,以及哪些属性是必填的或固定不变的。
在上面的例子中,我非常倾向于让一个 Checkbox 组件始终渲染一个 <input type="checkbox"> ,因此我不想让消费者覆盖 type 属性。
但情况并非总是如此!有时,我确实希望允许用户覆盖内置属性。
例如,假设我有一个生成 SVG 图标的组件:
function ArrowIcon({ size, ...delegated }) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width={size}
height={size}
>
<path
d="M 20 0 L 24 12 L 0 12 L 24 12 L 20 24"
stroke="black"
strokeLinecap="round"
{...delegated}
/>
</svg>
);
}默认情况下,这个组件会渲染一个带有圆角线条的黑色箭头,但我可以提供自己的覆盖样式:
<ArrowIcon stroke="red" strokeLinecap="square" />对于 {...delegated} 应该放在哪里,并没有对或错的答案。相反,这是一项我们可以利用的工具,用于决定我想给予使用此组件的开发人员多少权限和灵活性。
有时候,委托属性这个工具过于粗糙,我们需要做一些手动工作来解决冲突。
例如,在处理 CSS 类时,我们通常希望同时应用用户提供的类和内置的类。
在 Toggle 练习中,我们手动将两个类合并在一起,以便应用 toggle 类(提供所有标准 toggle 样式)以及 green-toggle 类(用户指定的类,用于覆盖 toggle 的颜色)。
我构建了许多遵循此精确模板的组件。以下是一个最小可行示例,所有其他内容都被移除:
function Template({ className = "" }) {
const appliedClass = `built-in-class ${className}`;
return <div className={appliedClass} />;
}从某种意义上说,我们其实已经见过这种模式的一个例子了,那就是在我们讨论 Hook 的规则时:
function TextInput({ id, label, type }) {
let generatedId = React.useId();
let appliedId = id || generatedId;
return (
<div className="text-input">
<label htmlFor={appliedId}>{label}</label>
<input id={appliedId} type={type} />
</div>
);
}如果用户提供了 id 属性,它将用于输入框的 id ,以及标签的 htmlFor 。如果没有提供,我们将使用从 React.useId 钩子(hook)中获得的生成值。
我们可以依赖 rest/spread 操作符在 <input> 上应用正确的 id ,但我们还需要通过 htmlFor 属性在 <label> 上设置完全相同的值。因此,我们需要手动处理这一冲突。
下面还有一个示例,展示如何向已经具有某些内联样式的组件提供自定义的内联样式:
function ExampleComponent({
// User-specified styles.
// Defaults to an empty object so that we always receive an
// object, never “undefined”:
style = {},
children,
...delegated
}) {
const builtInStyle = {
padding: 16,
background: "red",
};
return (
<div
{...delegated}
style={{
// Merge both sets of styles, prioritizing the
// built-in styles:
...style,
...builtInStyle,
}}
>
{children}
</div>
);
}回顾一下,在处理属性冲突时,我们有几种不同的选择。
这三种选项在不同情况下都是有效的。具体取决于我们想给予使用者多少控制权。