原文: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,是這樣工做的:this
要建立一個非受控 input
,要設置一個 defaultValue
屬性。這種狀況下 React 組件會使用底層 DOM 節點並藉助節點組件自己的 state 管理該 value。撇開實現細節不說,你能夠將之想象成調用了組件的 setState() 更新了 state.value 並將之賦值給了 DOM input 元素。spa
要建立一個受控 input
,則要設置 value
和 onChange()
屬性。在這種狀況下,一旦 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 組件, 只處理了一個布爾值屬性,因此我選擇用 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 關注咱們哦