前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >组件设计 —— 重新认识受控与非受控组件

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

作者头像
牧云云
发布2019-11-24 16:35:35
7990
发布2019-11-24 16:35:35
举报
文章被收录于专栏:云瓣

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

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

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

代码语言:javascript
复制
// 组件提供方
function Input({ defaultValue }) {
  return <input defaultValue={defaultValue} />
}

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

在受控组件中, 数值的展示与变更则分别由组件的 statesetState 接管。同样以 Input 组件为例:

代码语言:javascript
复制
// 组件提供方
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} /> 的最初调用方式:

代码语言:javascript
复制
// 组件提供方
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 组件是一个非受控组件。值得一提的是, 以非受控组件的使用方式去调用受控组件是一种反模式, 在下文中会分析其中的弊端。

如何做到不管对于组件提供方还是调用方 Input 组件都为受控组件呢? 提供方让出控制权即可, 调整代码如下codesandbox:

代码语言:javascript
复制
// 组件提供方
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)} />
}

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

职能范围

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

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

代码语言:javascript
复制
// 组件提供方
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)}
  />
}

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

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

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

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

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

代码语言:javascript
复制
// 组件提供方
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
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2019-11-23 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 重新定义受控与非受控组件的边界
  • 职能范围
  • 反模式 —— 以非受控组件的使用方式调用受控组件
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档