對React children 的深入理解

本文為翻譯文章,原文鏈接

React的核心為組件。你可以像嵌套HTML標簽一樣嵌套使用這些組件,這使得編寫JSX更加容易因為它類似于標記語言。

當我剛開始學習React時,當時我認為“使用 props.children 就這么回事,我知道它的一切”。我錯了。。

因為我們使用的事JavaScript,我們會改變children。我們能夠給它們發送特殊的屬性,以此來決定它們是否進行渲染。讓我們來探究一下React中children的作用。

子組件

我們有一個組件 <Grid /> 包含了幾個組件 <Row /> 。你可能會這么使用它:

<Grid>
  <Row />
  <Row />
  <Row />
</Grid>

這三個 Row 組件都成為了 Gridprops.children 。使用一個表達式容器,父組件就能夠渲染它們的子組件:

class Grid extends React.Component {
  render() {
    return <div>{this.props.children}</div>
  }
}

父組件也能夠決定不渲染任何的子組件或者在渲染之前對它們進行操作。例如,這個 <Fullstop /> 組件就沒有渲染它的子組件:

class Fullstop extends React.Component {
  render() {
    return <h1>Hello world!</h1>
  }
}

不管你將什么子組件傳遞給這個組件,它都只會顯示“Hello world!”

任何東西都能是一個child

React中的Children不一定是組件,它們可以使任何東西。例如,我們能夠將上面的文字作為children傳遞我們的 <Grid /> 組件。

<Grid>Hello world!</Grid>

JSX將會自動刪除每行開頭和結尾的空格,以及空行。它還會把字符串中間的空白行壓縮為一個空格。

這意味著以下的這些例子都會渲染出一樣的情況:

<Grid>Hello world!</Grid>

<Grid>
  Hello world!
</Grid>

<Grid>
  Hello
  world!
</Grid>

<Grid>

  Hello world!
</Grid>

你也可以將多種類型的children完美的結合在一起:

<Grid>
  Here is a row:
  <Row />
  Here is another row:
  <Row />
</Grid>

child 的功能

我們能夠傳遞任何的JavaScript表達式作為children,包括函數。

為了說明這種情況,以下是一個組件,它將執行一個傳遞過來的作為child的函數:

class Executioner extends React.Component {
  render() {
    // See how we're calling the child as a function?
    //                        ↓
    return this.props.children()
  }
}

你會像這樣的使用這個組件

<Executioner>
  {() => <h1>Hello World!</h1>}
</Executioner>

當然,這個例子并沒什么用,只是展示了這個想法。

假設你想從服務器獲取一些數據。你能使用多種方法實現,像這種將函數作為child的方法也是可行的。

<Fetch url="api.myself.com">
  {(result) => <p>{result}</p>}
</Fetch>

不要擔心這些超出了你的腦容量。我想要的是當你以后遇到這種情況時不再驚訝。有了children什么事都會發生。

操作children

如果你看過React的文檔你就會說“children是一個不透明的數據結構”。從本質上來講, props.children 可以使任何的類型,比如數組、函數、對象等等。

React提供了一系列的函數助手來使得操作children更加方便。

循環

兩個最顯眼的函數助手就是 React.Children.map 以及 React.Children.forEach 。它們在對應數組的情況下能起作用,除此之外,當函數、對象或者任何東西作為children傳遞時,它們也會起作用。

class IgnoreFirstChild extends React.Component {
  render() {
    const children = this.props.children
    return (
      <div>
        {React.Children.map(children, (child, i) => {
          // Ignore the first child
          if (i < 1) return
          return child
        })}
      </div>
    )
  }
}

<IgnoreFirstChild /> 組件在這里會遍歷所有的children,忽略第一個child然后返回其他的。

<IgnoreFirstChild>
  <h1>First</h1>
  <h1>Second</h1> // <- Only this is rendered
</IgnoreFirstChild>

在這種情況下,我們也可以使用 this.props.children.map 的方法。但要是有人講一個函數作為child傳遞過來將會發生什么呢?this.props.children 會是一個函數而不是一個數組,接著我們就會產生一個error!

err

然而使用 React.Children.map 函數,無論什么都不會報錯。

<IgnoreFirstChild>
  {() => <h1>First</h1>} // <- Ignored ??
</IgnoreFirstChild>

計數

因為this.props.children 可以是任何類型的,檢查一個組件有多少個children是非常困難的。天真的使用 this.props.children.length ,當傳遞了字符串或者函數時程序便會中斷。假設我們有個child:"Hello World!" ,但是使用 .length 的方法將會顯示為12。

這就是為什么我們有 React.Children.count 方法的原因

class ChildrenCounter extends React.Component {
  render() {
    return <p>React.Children.count(this.props.children)</p>
  }
}

無論時什么類型它都會返回children的數量

// Renders "1"
<ChildrenCounter>
  Second!
</ChildrenCounter>

// Renders "2"
<ChildrenCounter>
  <p>First</p>
  <ChildComponent />
</ChildrenCounter>

// Renders "3"
<ChildrenCounter>
  {() => <h1>First!</h1>}
  Second!
  <p>Third!</p>
</ChildrenCounter>

轉換為數組

如果以上的方法你都不適合,你能將children轉換為數組通過 React.Children.toArray 方法。如果你需要對它們進行排序,這個方法是非常有用的。

class Sort extends React.Component {
  render() {
    const children = React.Children.toArray(this.props.children)
    // Sort and render the children
    return <p>{children.sort().join(' ')}</p>
  }
}
<Sort>
  // We use expression containers to make sure our strings
  // are passed as three children, not as one string
  {'bananas'}{'oranges'}{'apples'}
</Sort>

上例會渲染為三個排好序的字符串。

sort

執行單一child

如果你回過來想剛才的 <Executioner /> 組件,它只能在傳遞單一child的情況下使用,而且child必須為函數。

class Executioner extends React.Component {
  render() {
    return this.props.children()
  }
}

我們可以試著去強制執行 propTypes ,就像下面這樣

Executioner.propTypes = {
  children: React.PropTypes.func.isRequired,
}

這會使控制臺打印出一條消息,部分的開發者將會把它忽視。相反的,我們可以使用在 render 里面使用 React.Children.only

class Executioner extends React.Component {
  render() {
    return React.Children.only(this.props.children)()
  }
}

這樣只會返回一個child。如果不止一個child,它就會拋出錯誤,讓整個程序陷入中斷——完美的避開了試圖破壞組件的懶惰的開發者。

編輯children

我們可以將任意的組件呈現為children,但是任然可以用父組件去控制它們,而不是用渲染的組件。為了說明這點,讓我們舉例一個 能夠擁有很多 RadioButton 組件的 RadiaGroup 組件。

RadioButtons 不會從 RadioGroup 本身上進行渲染,它們只是作為children使用。這意味著我們將會有這樣的代碼。

render() {
  return(
    <RadioGroup>
      <RadioButton value="first">First</RadioButton>
      <RadioButton value="second">Second</RadioButton>
      <RadioButton value="third">Third</RadioButton>
    </RadioGroup>
  )
}

這段代碼有一個問題。input 沒有被分組,導致了這樣:

為了把 input 標簽弄到同組,必須擁有相同的name 屬性。當然我們可以直接給每個RadioButtonname 賦值

<RadioGroup>
  <RadioButton name="g1" value="first">First</RadioButton>
  <RadioButton name="g1" value="second">Second</RadioButton>
  <RadioButton name="g1" value="third">Third</RadioButton>
</RadioGroup>

但是這個是無聊的并且容易出錯。我們可是擁有JavaScript的所有功能的!

改變children的屬性

RadioGroup 中我們將會添加一個叫做 renderChildren 的方法,在這里我們編輯children的屬性

class RadioGroup extends React.Component {
  constructor() {
    super()
    // Bind the method to the component context
    this.renderChildren = this.renderChildren.bind(this)
  }

  renderChildren() {
    // TODO: Change the name prop of all children
    // to this.props.name
    return this.props.children
  }

  render() {
    return (
      <div className="group">
        {this.renderChildren()}
      </div>
    )
  }
}

讓我們開始遍歷children獲得每個child

renderChildren() {
  return React.Children.map(this.props.children, child => {
    // TODO: Change the name prop to this.props.name
    return child
  })
}

我們如何編輯它們的屬性呢?

永恒地克隆元素

這是今天展示的最后一個輔助方法。顧名思義,React.cloneElement 會克隆一個元素。我們將想要克隆的元素當作第一個參數,然后將想要設置的屬性以對象的方式作為第二個參數。

const cloned = React.cloneElement(element, {
  new: 'yes!'
})

現在,clone 元素有了設置為 "yes!" 的屬性 new

這正是我們的 RadioGroup 所需的。我們克隆所有的child并且設置name 屬性

renderChildren() {
  return React.Children.map(this.props.children, child => {
    return React.cloneElement(child, {
      name: this.props.name
    })
  })
}

最后一步就是傳遞一個唯一的 nameRadioGroup

<RadioGroup name="g1">
  <RadioButton value="first">First</RadioButton>
  <RadioButton value="second">Second</RadioButton>
  <RadioButton value="third">Third</RadioButton>
</RadioGroup>

沒有手動添加 name 屬性給所有的 RadioButton ,我們只是告訴了 RadioGroup 所需的name而已。

總結

Children使React組件更像是標記而不是 脫節的實體。通過強大的JavaScript和一些React幫助函數使我們的生活更加簡單。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,563評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,694評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,672評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,965評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,690評論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,019評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,013評論 3 449
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,188評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,718評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,438評論 3 360
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,667評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,149評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,845評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,252評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,590評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,384評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,635評論 2 380

推薦閱讀更多精彩內容