组件设计 —— 从新认识受控与非受控组件

从新定义受控与非受控组件的边界

React 官网中对非受控组件与受控组件做了如图中下划线的边界定义。一经推敲, 该定义是缺少了些完整性严谨性的, 好比针对非表单组件(弹框、轮播图)如何划分受控与非受控的边界? 又好比非受控组件是否真的如文案上所说的数据的展现与变动都由 dom 自身接管呢?html

在非受控组件中, 一般业务调用方只需传入一个初始默认值即可使用该组件。以 Input 组件为例:react

// 组件提供方
function Input({ defaultValue }) {
  return <input defaultValue={defaultValue} />
}

// 调用方
function Demo() {
  return <Input defaultValue={1} />
}

在受控组件中, 数值的展现与变动则分别由组件的 statesetState 接管。一样以 Input 组件为例:git

// 组件提供方
function Input() {
  const [value, setValue] = React.useState(1)
  return <input value={value} onChange={e => setValue(e.target.value)} />
}

// 调用方
function Demo() {
  return <Input />
}

有意思的一个问题来了, Input 组件究竟是受控的仍是非受控的? 咱们甚至还能够对代码稍加改动成 <Input defaultValue={1} /> 的最初调用方式:github

// 组件提供方
function Input({ defaultValue }) {
  const [value, setValue] = React.useState(defaultValue)
  return <input value={value} onChange={e => setValue(e.target.value)} />
}

// 调用方
function Demo() {
  return <Input defaultValue={1} />
}

尽管此时 Input 组件自己是一个受控组件, 但与之相对的调用方失去了更改 Input 组件值的控制权, 因此对调用方而言, Input 组件是一个非受控组件。值得一提的是, 以非受控组件的使用方式去调用受控组件是一种反模式, 在下文中会分析其中的弊端。dom

如何作到无论对于组件提供方仍是调用方 Input 组件都为受控组件呢? 提供方让出控制权便可, 调整代码以下codesandbox:code

// 组件提供方
function Input({ value, onChange }) {
  return <input value={value} onChange={onChange} />
}

// 调用方
function Demo() {
  const [value, setValue] = React.useState(1)
  return <Input value={value} onChange={e => setValue(e.target.value)} />
}

通过上述代码的推演后, 归纳以下: 受控以及非受控组件的边界划分取决于当前组件对于子组件值的变动是否拥有控制权。如如有则该子组件是当前组件的受控组件; 如若没有则该子组件是当前组件的非受控组件。component

职能范围

基于调用方对于受控组件拥有控制权这一认知, 所以受控组件相较非受控组件能赋予调用方更多的定制化职能。这一思路与软件开发中的开放/封闭原则有殊途同归之妙, 同时让笔者受益不浅的 Inversion of Control 也是相似的思想。htm

借助受控组件的赋能, 以 Input 组件为例, 好比调用方能够更为自由地对值进行校验限制, 又好比在值发生变动时执行一些额外逻辑。blog

// 组件提供方
function Input({ value, onChange }) {
  return <input value={value} onChange={onChange} />
}

// 调用方
function Demo() {
  const [value, setValue] = React.useState(1)
  return <Input value={value} onChange={e =>
    // 只支持数值的变动
    if (/\D/.test(e.target.value)) return
    setValue(e.target.value)}
  />
}

所以综合基础组件扩展性通用性的考虑, 受控组件的职能相较非受控组件更加宽泛, 建议优先使用受控组件来构建基础组件。ci

反模式 —— 以非受控组件的使用方式调用受控组件

首先何谓反模式? 笔者将其总结为增大隐性 bug 出现几率的模式, 该模式是最佳实践的对立经验。如若使用了反模式就不得不花更多的精力去避免潜在 bug。官网对反模式也有很好的归纳总结

缘何上文提到以非受控组件的使用方式去调用受控组件是一种反模式? 观察 Input 组件的第一行代码, 其将 defaultValue 赋值给 value, 这种将 props 赋值给 state 的赋值行为在必定程度上会增长某些隐性 bug 的出现几率。

好比在切换导航栏的场景中, 恰巧两个导航中传进组件的 defaultValue 是相同的值, 在导航切换的过程当中便会将导航一中的 Input 的状态值带到导航二中, 这显然会让使用方感到困惑。codesandbox

// 组件提供方
function Input({ defaultValue }) {
  // 反模式
  const [value, setValue] = React.useState(defaultValue);
  React.useEffect(() => {
    setValue(defaultValue);
  }, [defaultValue]);
  return <input value={value} onChange={e => setValue(e.target.value)} />;
}

// 调用方
function Demo({ defaultValue }) {
  return <Input defaultValue={defaultValue} />;
}

function App() {
  const [tab, setTab] = React.useState(1);
  return (
    <>
      {tab === 1 ? <Demo defaultValue={1} /> : <Demo defaultValue={1} />}
      <button onClick={() => (tab === 1 ? setTab(2) : setTab(1))}>
        切换 Tab
      </button>
    </>
  );
}

如何避免使用该反模式同时有效解决问题呢? 官方提供了两种较为优质的解法, 将其留给你们做为思考。

  1. 方法一: 使用彻底受控组件(更为推荐)
  2. 方法二: 使用彻底非受控组件 + key

欢迎关注 personal blog

相关文章
相关标签/搜索