[譯] React 中的受控組件和非受控組件

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

你可曾踟躕過該建立受控組件仍是非受控組件呢?java

一些背景

若是初涉 React 應用開發,你可能曾嘀咕過:「受控組件和非受控組件是啥?」。那麼我建議你額外花點時間先看看官網的文檔。react

在 React 應用中之因此須要受控組件和非受控組件,原由於<input><textarea><select> 這類特定的 DOM 元素默認在 DOM 層中維持狀態(用戶輸入)。受控組件用來在 React 中也保存該狀態,好比同步到渲染輸入元素的組件、樹結構中的某個父組件,或者一個 flux store 中。app

而這種模式能夠被擴展至特定的非 DOM 狀態相關的用例中。好比,在最近的一個應用中,我須要建立一個可嵌套的 Collapsible 摺疊組件,支持兩種操做模式:某些狀況下須要使其被外界可控(當應用中的其餘區域發生用戶交互時擴展開),其餘時候它能簡單的本身管理狀態就能夠了。ui

React 中的 Inputs

對於 React 中的 Inputs,是這樣工做的:this

要建立一個非受控 input,要設置一個 defaultValue 屬性。這種狀況下 React 組件會使用底層 DOM 節點並藉助節點組件自己的 state 管理該 value。撇開實現細節不說,你能夠將之想象成調用了組件的 setState() 更新了 state.value 並將之賦值給了 DOM input 元素。spa

要建立一個受控 input,則要設置 valueonChange() 屬性。在這種狀況下,一旦 value 屬性改變,React 總會將該屬性賦值給 input 做爲它的值。當用戶改變了 input 的值,onChange() 回調會被調用,並必須當即得出一個新的 value 屬性值用以發送給 input。所以,若是 onChange() 沒被正確的處理,則 input 實際上就成了只讀;由於 input 老是靠着 value 屬性來渲染其值的,用戶也就沒法改變 input 的值了。code

通常模式

還好,利用這種行爲建立組件不算麻煩。關鍵在於建立一個組件接口,能夠在兩種可能的屬性配置中選擇其一。component

要建立一個非受控組件,就將想控制的屬性定義成 defaultXXX。當一個被定義了 defaultXXX 屬性的組件初始化時,將以給定的值開始,並在組件的生命週期中自我管理狀態(調用 setState() 以響應用戶交互)。這就覆蓋了用例1:組件無須被外部控制且狀態本地化。cdn

要建立一個受控組件,首先定義好想要控制的屬性 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 關注咱們哦

相關文章
相關標籤/搜索