My App

冲突

冲突

上一课的“切换”练习中,我们看到了如果存在冲突,将所有 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" 覆盖。

API 设计中的一个强大工具

在编写 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>
  );
}

回顾一下,在处理属性冲突时,我们有几种不同的选择。

  1. 如果我们希望允许使用者覆盖某个特定的硬编码属性,可以在其后使用 {...delegated} 语法。
  2. 然而,如果我们希望优先使用硬编码属性,则应将 {...delegated} 语法放在前面。
  3. 如果我们想要合并两个值,则需要我们自己进行管理,这时不使用 {...delegated} 。

这三种选项在不同情况下都是有效的。具体取决于我们想给予使用者多少控制权。

On this page