当前位置: 首页 > news >正文

[译] React 中的受控组件和非受控组件

原文:https://www.viget.com/articles/controlling-components-react/

你可曾踟蹰过该创建受控组件还是非受控组件呢?

一些背景

如果初涉 React 应用开发,你可能曾嘀咕过:“受控组件和非受控组件是啥?”。那么我建议你额外花点时间先看看官网的文档。

在 React 应用中之所以需要受控组件和非受控组件,起因于<input><textarea><select> 这类特定的 DOM 元素默认在 DOM 层中维持状态(用户输入)。受控组件用来在 React 中也保存该状态,比如同步到渲染输入元素的组件、树结构中的某个父组件,或者一个 flux store 中。

而这种模式可以被扩展至特定的非 DOM 状态相关的用例中。比如,在最近的一个应用中,我需要创建一个可嵌套的 Collapsible 折叠组件,支持两种操作模式:某些情况下需要使其被外界可控(当应用中的其他区域发生用户交互时扩展开),其他时候它能简单的自己管理状态就可以了。

React 中的 Inputs

对于 React 中的 Inputs,是这样工作的:

要创建一个非受控 input,要设置一个 defaultValue 属性。这种情况下 React 组件会使用底层 DOM 节点并借助节点组件本身的 state 管理该 value。撇开实现细节不说,你可以将之想象成调用了组件的 setState() 更新了 state.value 并将之赋值给了 DOM input 元素。

要创建一个受控 input,则要设置 valueonChange() 属性。在这种情况下,一旦 value 属性改变,React 总会将该属性赋值给 input 作为它的值。当用户改变了 input 的值,onChange() 回调会被调用,并必须立即得出一个新的 value 属性值用以发送给 input。因此,如果 onChange() 没被正确的处理,则 input 实际上就成了只读;因为 input 总是靠着 value 属性来渲染其值的,用户也就无法改变 input 的值了。

一般模式

还好,利用这种行为创建组件不算麻烦。关键在于创建一个组件接口,可以在两种可能的属性配置中选择其一。

要创建一个非受控组件,就将想控制的属性定义成 defaultXXX。当一个被定义了 defaultXXX 属性的组件初始化时,将以给定的值开始,并在组件的生命周期中自我管理状态(调用 setState() 以响应用户交互)。这就覆盖了用例1:组件无须被外部控制且状态本地化。

要创建一个受控组件,首先定义好想要控制的属性 xxx。组件以 xxx 属性给定的值和一个用于响应 xxx 改变的回调方法(例如 xxx 是布尔值的话,响应的就是 toggleXXX())被初始化。当用户对该组件做出交互,不同于非受控组件在内部调用了 setState() 的是,组件必须调用 toggleXXX() 回调以请求外部更新相关 state 值。更新过后,容器组件应该以重新渲染并向受控组件发送一个 xxx 值才告一段落。

Collapsible 接口

对于开头提到的 Collapsible 组件, 只处理了一个布尔值属性,所以我选择用 collapsed / defaultCollapsed 和 toggleCollapsed() 作为组件的接口。

当指定一个 defaultCollapsed 属性后,Collapsible 组件将以该属性所声明的状态开始工作,但在其生命周期自我管理状态。点击子按钮会出发一个 setState() 并更新内部组件状态。

而指定一个布尔值的 collapsed 属性以及一个 toggleCollapsed() 回调属性的话,Collapsible 组件也会以 collapsed 属性所声明的值开始工作,但点击的时候,只会去调用 toggleCollapsed() 回调。理想的状况是,由 toggleCollapsed() 更新外层某个组件中的状态,并引发 Collapsible 组件由于得到了新的 collapsed 属性而重新渲染。

实现

有一种非常简单的模式适用于本项工作,其主要思路如下:

当组件被初始化时,将 xxx 传入的值或 xxx 的默认值放入 state 中。在本例中,defaultCollapsed 的默认值是 false。

在渲染阶段,如果定义了 xxx 属性,那么按其行事(受控模式);否则就在 this.state 中使用本地组件的值(非受控模式)。这意味着在 Collapsible 组件的 render 方法中,我是这么决定 collapsed 状态的:

let collapsed = this.props.hasOwnProperty('collapsed') 
    ? this.props.collapsed 
    : this.state.collapsed
复制代码

利用解构和默认值,也可以让写法更优雅一些:

// 覆盖了受控和非受控两种用例下的状态选择
const {
  collapsed = this.state.collapsed,
  toggleCollapsed
} = this.props
复制代码

以上代码的意思就是:“给我一个叫做 collapsed 的绑定,从 this.props.collapsed 中取它的值;不过要是那个值没定义,就用 this.state.collapsed 代替”。

封装

对于使你自己的组件同时支持可控/非可控行为这一点上,你应该能明白这是简单而很可能有用的。希望你能清楚的理解为什么需要用这种方式构建组件,并且也知道如何去做。以下正是你所好奇的 Collapsible 组件的完整源码 -- 很简短的。

/**
 * Collapsible 是一个高阶组件,为一个给定的组件提供了可折叠行为。
 * 基于其 `collapsed` 属性,被包装的组件可以决定如何渲染。
 */
import invariant from 'invariant'
import { createElement, Component } from 'react'
import getDisplayName from 'recompose/getDisplayName'
import hoistStatics from 'hoist-non-react-statics'
import PropTypes from 'prop-types'

export default function collapsible(WrappedComponent) {
  invariant(
    typeof WrappedComponent == 'function',
    `You must pass a component to the function returned by ` +
    `collapsible. Instead received ${JSON.stringify(WrappedComponent)}`
  )

  const wrappedComponentName = getDisplayName(WrappedComponent)
  const displayName = `Collapsible(${wrappedComponentName})`

  class Collapsible extends Component {

    static displayName = displayName
    static WrappedComponent = WrappedComponent

    static propTypes = {
      onToggle: PropTypes.func,
      collapsed: PropTypes.bool,
      defaultCollapsed: PropTypes.bool
    }

    static defaultProps = {
      onToggle: () => {},
      collapsed: undefined,
      defaultCollapsed: true
    }

    constructor(props, context) {
      super(props, context)

      this.state = {
        collapsed: props.defaultCollapsed
      }
    }

    render() {
      const {
        collapsed = this.state.collapsed, // 魔术开始了
        defaultCollapsed,
        ...props
      } = this.props

      return createElement(WrappedComponent, {
        ...props,
        collapsed,
        toggleCollapsed: this.toggleCollapsed
      })
    }

    toggleCollapsed = () => {
      this.setState(({ collapsed }) => ({ collapsed: !collapsed }))
      this.props.onToggle()
    }
  }

  return hoistStatics(Collapsible, WrappedComponent)
}
复制代码
----------------------------------------

长按二维码或搜索 fewelife 关注我们哦

相关文章:

  • drbd配置简述
  • 聊聊编译时注解
  • 微服务架构—高级设计篇
  • 漫谈版本控制系统
  • pandas(一)操作Series和DataFrame的基本功能
  • Redhat6.5匿名访问win10共享文件夹.
  • 从零开始在ubuntu上搭建node开发环境
  • 《CDN 之我见》原理篇——CDN的由来与调度
  • 汇编语言第三版答案(王爽)
  • RSACryptoServiceProvider加密解密签名验签和DESCryptoServiceProvider加解密
  • 算法之不定期更新(一)(2018-04-12)
  • Java一行代码控制shape 优雅的解决 Drawable Shape 文件繁多问题
  • innerWidth outerWidth
  • 共享单车运营方不能“只管生不管养”
  • 多项底层技术发力,我国物联网大规模商用迎来窗口期
  • [译] React v16.8: 含有Hooks的版本
  • 《Java编程思想》读书笔记-对象导论
  • css布局,左右固定中间自适应实现
  • quasar-framework cnodejs社区
  • SOFAMosn配置模型
  • web标准化(下)
  • 对超线程几个不同角度的解释
  • 基于Vue2全家桶的移动端AppDEMO实现
  • 力扣(LeetCode)22
  • 如何设计一个比特币钱包服务
  • 什么软件可以剪辑音乐?
  • 使用parted解决大于2T的磁盘分区
  • 通过npm或yarn自动生成vue组件
  • 线性表及其算法(java实现)
  • 智能网联汽车信息安全
  • 如何在招聘中考核.NET架构师
  • 曜石科技宣布获得千万级天使轮投资,全方面布局电竞产业链 ...
  • 昨天1024程序员节,我故意写了个死循环~
  • ​MySQL主从复制一致性检测
  • ### Cause: com.mysql.jdbc.exceptions.jdbc4.MySQLTr
  • #if 1...#endif
  • #mysql 8.0 踩坑日记
  • #基础#使用Jupyter进行Notebook的转换 .ipynb文件导出为.md文件
  • #在线报价接单​再坚持一下 明天是真的周六.出现货 实单来谈
  • (2)MFC+openGL单文档框架glFrame
  • (26)4.7 字符函数和字符串函数
  • (6)设计一个TimeMap
  • (Forward) Music Player: From UI Proposal to Code
  • (四) Graphivz 颜色选择
  • .NET 4.0网络开发入门之旅-- 我在“网” 中央(下)
  • .NET单元测试
  • .Net开发笔记(二十)创建一个需要授权的第三方组件
  • .NET开发不可不知、不可不用的辅助类(一)
  • .NET开源项目介绍及资源推荐:数据持久层 (微软MVP写作)
  • .NET下ASPX编程的几个小问题
  • .stream().map与.stream().flatMap的使用
  • @PreAuthorize注解
  • [ vulhub漏洞复现篇 ] JBOSS AS 5.x/6.x反序列化远程代码执行漏洞CVE-2017-12149
  • [1159]adb判断手机屏幕状态并点亮屏幕
  • [20180129]bash显示path环境变量.txt