React cloneWithProps 與實作 Tabs

實作一個 Tabs 元件

複合式(組合)元件

在 React 中任何東西都是元件,就像樂高一樣,你可以用小片的積木組成大塊的,再組合出您想到的東西。
同樣的道理您也可以用許多的小元件(小功能模組)來組合出您的應用程式。所謂的複合式元件或稱作組合元件,
他其實就是由多個元件去組成一個多功能的大元件。

在這篇文章我們要來建立一個 tabs 標簽切換功能的元件,為了達成這個功能我們需要 4 個不同的元件:
<Tabs />, <TabList />, <Tab /><TabPanel /> 分別用來呈現整個 tabs , 列出
標簽列,標簽列的按鈕,以及顯示的內容。結構如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<Tabs>
<TabList>
<Tab>Iron man</Tab>
<Tab>Superman</Tab>
<Tab>Lucy</Tab>
</TabList>
<TabPanel>
鋼鐵人介紹
</TabPanel>

<TabPanel>
超人介紹
</TabPanel>

<TabPanel>
鹿茸介紹
</TabPanel>
</Tabs>

首先是 <Tabs/> 的行為,它被用來當做一個容器,其角色有點像是一個 controller ,因為它必須要掌管所有 DOM 的事件(點擊 Tab 切換至該內容,被選取到的 index)
同時也需要管理 state 看看哪個 <Tab/> 目前正被選取到,所以我們會稱 <Tabs /> 為擁有者元件(owner component)。

我們遭遇到的第一個挑戰是: 元件之間該如何溝通。每一個元件都有一個 state 。每當 state 發生變動,React 就會更新並重新渲染元件以使其跟 state 一致。
當我們選了某個索引後,<Tabs/> 元件就要去更新 <Tab/><TabPanel/>state

典範轉移

首先我們為每一個元件建立一個 API,透過建立一個方法來變更 state。在 React 中一個元件可以透過 this.props.children 去存取子元件。
所以一開始我們理論上只要使用 handleSelected 去設定適當該顯示的子元件如下:

1
2
3
4
5
var tabs = this.props.children[0].props.children,
panels = this.props.children.slice(1),
index = this.state.selectedIndex;
tabs[index].handleSelected(true);
panels[index].handleSelected(true);

這樣的做法在 v0.10.0 以前的版本是可以運作的,概略的實作如下:

/**
 * @jsx React.DOM
 */


var Tab = React.createClass({
getInitialState: function () {
return {selected: false}
},
handleSelected: function (status) {
this.setState({selected: status});
},
render: function () {
var cx = React.addons.classSet;
var classes = cx({
'react-tab': true,
'active': this.state.selected
});
return (
<li className={classes}>
<a href='#' data-index={this.props.index}>{this.props.children}</a>
</li>
);
}
});

var TabList = React.createClass({
render: function () {
return (
<ul className='react-tab-list'>
{this.props.children}
</ul>
);
}
});

var Tabs = React.createClass({
getInitialState: function () {
return {selectedIndex: 0}
},
componentDidMount: function () {
var tabs = this.props.children[0].props.children,
panels = this.props.children.slice(1),
index = this.state.selectedIndex;
for (i in tabs) {
if (i == index) {
tabs[i].handleSelected(true);
panels[i].handleSelected(true);
} else {
tabs[i].handleSelected(false);
panels[i].handleSelected(false);
}
}
},
handleClick: function (e) {
var index = parseInt(e.target.getAttribute('data-index'));
var tabs = this.props.children[0].props.children,
panels = this.props.children.slice(1);
for (i in tabs) {
if (i == index) {
tabs[i].handleSelected(true);
panels[i].handleSelected(true);
} else {
tabs[i].handleSelected(false);
panels[i].handleSelected(false);
}
}

},
render: function () {
return (
<div className='react-tabs' onClick={this.handleClick}>
{this.props.children}
</div>
);
}
});

var TabPanel = React.createClass({
getInitialState: function () {
return {selected: false}
},
handleSelected: function (status) {
console.log(this.props.name + ' selected: ' + status);
this.setState({selected: status});
},
render: function () {
var cx = React.addons.classSet;
var classes = cx({
'react-tab-panel': true,
'active': this.state.selected
});
return (
<div className={classes}>
{this.props.children}
</div>
)
}
});

var App = React.createClass({
render: function () {
return (
<div className='container'>
<Tabs>
<TabList>
<Tab name='ironman' index={0}>Iron man</Tab>
<Tab name='superman' index={1}>Superman</Tab>
<Tab name='lucy' index={2}>Lucy</Tab>
</TabList>

      &lt;TabPanel name=&#x27;panel-ironman&#x27;&gt;
      鋼鐵人
      &lt;/TabPanel&gt;

      &lt;TabPanel name=&#x27;panel-superman&#x27;&gt;
      超人再起
      &lt;/TabPanel&gt;

      &lt;TabPanel name=&#x27;panel-lucy&#x27;&gt;
      露西
      &lt;/TabPanel&gt;
    &lt;/Tabs&gt;
  &lt;/div&gt;
);

}
});

React.renderComponent(
<App />,
document.getElementById('example')
);

See the Pen oitzv by AndyYou (@AndyYou) on CodePen.

不過到了 React v0.10.0 版本的時候這樣做會出現警告:

1
Invalid access to component property "setSelected"

到了 v0.11.0 的時候更慘您已經無法直接存取子元件的方法,因為是新版的 React this.props.children 回傳的物件只是描述物件(discriptors)。不再是對應元件的參考物件,且官方建議您不應該直接存取子元件的實際物件。

Component Refs

React 提供一種機制給你取得實際元件的物件,就是使用 refs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var App = React.createClass({
handleClick: function () {
alert(this.refs.myInput.getDOMNode().value);
},

render: function () {
return (
<div>
<input ref="myInput"/>
<button
onClick={this.handleClick}>
Submit
</button>
</div>
);
}
});

透過 refs 屬性您就可以取得該子元件的參考

動態的子元件

典型的 refs 使用方式是父元件已經知道子元件的情況,所以可以直接在 tag 中指定 ref 如上面的範例。
不過這次我們希望我們的 Tabs 元件可以動態的放入 <Tab/><TabPanel/>
例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
var App = React.createClass({
render: function () {
return (
<div className='container'>
<Tabs>
<TabList>
<Tab name='ironman' >Iron man</Tab>
<Tab name='superman' >Superman</Tab>
<Tab name='lucy' >Lucy</Tab>
</TabList>

<TabPanel name='panel-ironman'>
鋼鐵人
</TabPanel>

<TabPanel name='panel-superman'>
超人再起
</TabPanel>

<TabPanel name='panel-lucy'>
露西
</TabPanel>
</Tabs>
</div>
);
}
});

而 Tabs 只是動態地把開發者加入的任意結構輸出

1
2
3
4
5
6
7
8
9
var Tabs = React.createClass({
render: function () {
return (
<div className='react-tabs'>
{this.props.children}
</div>
);
}
});

因此我們需要一些動態指定 refs 的方法,而這個方法就是透過 cloneWithProps

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var App = React.createClass({
render: function () {
var index = 0,
children = React.Children.map(this.props.children, function (child) {
return React.addons.cloneWithProps(child, {
ref: 'child-' + (index++)
});
});

return (
<div>
{children}
</div>
);
}
});
作者

andyyou(YOU,ZONGYAN)

發表於

2014-09-16

更新於

2021-12-12

許可協議