React Day 5 - Reusable Components

當在設計界面的時候, 用規劃好的界面結構並拆分常見的設計元素(按鈕, 表單欄位, 排版元件… 等)成可重覆利用的組件. 這樣當你下次需要再建立相同的界面時, 你可以編寫比較少的程式碼, 這意謂著將會得到更快的開發速度, 少一點的錯誤和打字.

Prop Validation

當在開發你的程式時, Prop Validation 可以幫助讓你的組件使用正確. 我們可以透過指定一個 propTypes 屬性來作到這點. React.PropTypes 提供了很多種驗證讓你確保接受到的資料類型是正確的. 當收到一個不正確類型的資料內容, 一個警告通知會顯示在 Javascript Console.

需要注意的是基於效能考量, propTypes 應只在開發階段時使用. 下面將示範並說明有哪些驗證可以使用:

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
React.createClass({
propTypes: {
// You can declare that a prop is a specific JS primitive. By default, these
// are all optional.
optionalArray: React.PropTypes.array,
optionalBool: React.PropTypes.bool,
optionalFunc: React.PropTypes.func,
optionalNumber: React.PropTypes.number,
optionalObject: React.PropTypes.object,
optionalString: React.PropTypes.string,

// Anything that can be rendered: numbers, strings, elements or an array
// containing these types.
optionalNode: React.PropTypes.node,

// A React element.
optionalElement: React.PropTypes.element,

// You can also declare that a prop is an instance of a class. This uses
// JS's instanceof operator.
optionalMessage: React.PropTypes.instanceOf(Message),

// You can ensure that your prop is limited to specific values by treating
// it as an enum.
optionalEnum: React.PropTypes.oneOf(['News', 'Photos']),

// An object that could be one of many types
optionalUnion: React.PropTypes.oneOfType([
React.PropTypes.string,
React.PropTypes.number,
React.PropTypes.instanceOf(Message)
]),

// An array of a certain type
optionalArrayOf: React.PropTypes.arrayOf(React.PropTypes.number),

// An object with property values of a certain type
optionalObjectOf: React.PropTypes.objectOf(React.PropTypes.number),

// An object taking on a particular shape
optionalObjectWithShape: React.PropTypes.shape({
color: React.PropTypes.string,
fontSize: React.PropTypes.number
}),

// You can chain any of the above with `isRequired` to make sure a warning
// is shown if the prop isn't provided.
requiredFunc: React.PropTypes.func.isRequired,

// A value of any data type
requiredAny: React.PropTypes.any.isRequired,

// You can also specify a custom validator. It should return an Error
// object if the validation fails. Don't `console.warn` or throw, as this
// won't work inside `oneOfType`.
customProp: function(props, propName, componentName) {
if (!/matchme/.test(props[propName])) {
return new Error('Validation failed!');
}
}
},
/* ... */
});

Default Prop Values

React 可讓你描述預設 props 的資料內容:

1
2
3
4
5
6
7
8
var ComponentWithDefaultProps = React.createClass({
getDefaultProps: function() {
return {
value: 'default value'
};
}
/* ... */
});

getDefaultProps() 的結果會被快取與使用, 確保當 this.props.value 沒有經由上層組件 (parent component) 傳遞進來時仍有預設的值. 這讓你可以安心地使用你的 props 而不用為了顧及資料的存在與否而編寫一些重覆瑣碎的程式碼.

Transferring Props: A Shortcut

一般的 React component 只需要簡單擴充一個基本的 HTML. 你將會常需要從你的組件複製任何的 HTML attributes 到底層的 HTML 元素以減少打字. 你可以使用 JSX spread syntax 來實現這點:

1
2
3
4
5
6
7
8
9
10
11
12
13
var CheckLink = React.createClass({
render: function() {
// This takes any props passed to CheckLink and copies them to <a>
return <a {...this.props}>{'√ '}{this.props.children}</a>;
}
});

React.render(
<CheckLink href="/checked.html">
Click here!
</CheckLink>,
document.getElementById('example')
);

Single Child

透過 React.PropTypes.element 你可以指定組件只能擁有一個子元素:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var MyComponent = React.createClass({
propTypes: {
children: React.PropTypes.element.isRequired
},

render: function() {
return (
<div>
{this.props.children} // This must be exactly one element or it will throw.
</div>
);
}

});

Mixins

在 React 中最棒的一件事就是可以重覆使用程式碼, 但有時不同的組件可能需要相同的功能, 這樣的需求有時被稱作 cross-cutting concerns. React 提供 mixins 的機制解決這樣的問題.

常見的案例是有一個組件要定時地更新它自己. 可以用 setInterval() 輕易地作到這點, 但重點是當你不需要的時候取消定時這個機制以節約記憶體的浪費. React 提供 lifecyle methods 的一些方法來讓你知道一個組件什麼時候被建立和銷毁. 讓我們建立一個簡單的 mixin 並用這些方法來提供一個簡易並在組件被銷毁時能自動清除的 setInterval() 方法.

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
28
29
30
31
32
33
34
35
36
var SetIntervalMixin = {
componentWillMount: function() {
this.intervals = [];
},
setInterval: function() {
this.intervals.push(setInterval.apply(null, arguments));
},
componentWillUnmount: function() {
this.intervals.map(clearInterval);
}
};

var TickTock = React.createClass({
mixins: [SetIntervalMixin], // Use the mixin
getInitialState: function() {
return {seconds: 0};
},
componentDidMount: function() {
this.setInterval(this.tick, 1000); // Call a method on the mixin
},
tick: function() {
this.setState({seconds: this.state.seconds + 1});
},
render: function() {
return (
<p>
React has been running for {this.state.seconds} seconds.
</p>
);
}
});

React.render(
<TickTock />,
document.getElementById('example')
);

Mixins 有一個很棒的特色就是如果一個組件同時使用數個 mixins 而那些又同時定義了相同的 lifecyle method (例如這幾個 mixins 都同時在組件銷毁時做了一些清除動作), 所有的 lifecyle methods 都會確保有被呼叫. Mixins 的執行會依據你在 mixins 屬性定義的先後順序.