本文為翻譯文章,原文鏈接
React的核心為組件。你可以像嵌套HTML標簽一樣嵌套使用這些組件,這使得編寫JSX更加容易因為它類似于標記語言。
當我剛開始學習React時,當時我認為“使用 props.children
就這么回事,我知道它的一切”。我錯了。。
因為我們使用的事JavaScript,我們會改變children。我們能夠給它們發送特殊的屬性,以此來決定它們是否進行渲染。讓我們來探究一下React中children的作用。
子組件
我們有一個組件 <Grid />
包含了幾個組件 <Row />
。你可能會這么使用它:
<Grid>
<Row />
<Row />
<Row />
</Grid>
這三個 Row
組件都成為了 Grid
的 props.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!
然而使用 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>
上例會渲染為三個排好序的字符串。
執行單一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
屬性。當然我們可以直接給每個RadioButton
的name
賦值
<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
})
})
}
最后一步就是傳遞一個唯一的 name
給RadioGroup
<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幫助函數使我們的生活更加簡單。