Goal
Giới thiệu mối quan hệ của
props
và
state
trong Reactjs
props
là IF, state
là trạng thái của Component.
- state có nghĩa là ‘Trạng thái’. Từ bây giờ chúng ta mặc định dùng ‘state’ để chỉ trạng thái của Component.
Hãy xem xét ví dụ sau:
<!DOCTYPE html>
<html>
<head>
<meta http-equiv='Content-type' content='text/html; charset=utf-8'>
<title>Basic Example Props</title>
</head>
<body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.13.3/react.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.13.3/JSXTransformer.js"></script>
<script type="text/jsx">
var Avatar = React.createClass({
propTypes: {
name: React.PropTypes.string.isRequired,
width: React.PropTypes.number.isRequired,
height: React.PropTypes.number.isRequired,
like: React.PropTypes.bool.isRequired,
},
getInitialState() {
return {
liked: this.props.like
};
},
onClick() {
this.setState({liked: !this.state.liked});
},
render() {
var textLike = this.state.liked ? 'like' : 'haven\'t liked';
return (
<div>
<img src={this.props.src} width={this.props.width} height={this.props.height} alt="alt" />
<span>{this.props.name}</span>
<button onClick={this.onClick}>{textLike}</button>
</div>
);
}
});
var AvatarEl = <Avatar
name="Foo"
width={100}
height={100}
src='http://canime.files.wordpress.com/2010/05/mask-dtb.jpg'
like={false}
/>;
var AvatarComponent = React.render(AvatarEl, document.body);
</script>
</body>
</html>
Ví dụ trên được biên tập từ ví dụ trong
bài trước nhưng có một số thay đổi như sau:
- image source không còn được xem như là state của Component Avatar nữa.
- state của Component là like.
- like có kiểu dữ liệu là boolean.
- Giá trị liked (của
state
) được truyền và gán vào ở method getInitialState()
.
Chúng ta có thể config Avatar thông qua
props
của nó như là
name
,
width
,
height
,
src
,
like
. Còn khi trạng thái của nút
button
thì được chứa trong
state
. Hình bên dưới sẽ minh họa rõ hơn về điều này:
Bây giờ bạn hãy chạy source code trên và thử click vào button, bạn sẽ thấy text của button thay đổi. Như vậy bên trong Component được quản lý bởi
state
còn
props
chỉ dùng để ‘config’.
Khi thiết kế Component, thì trước hết hãy thiết kế Prop như là I/F, rồi định nghĩa những giá trị có thay đổi mà Component quản lý như là State.
Props in getInitialState Is an Anti-Pattern
Sử dụng
props
truyền vào để khởi tại
state
trong
getInitialState
thường dẫn đến việc duplication của nguyên tắc
Source Of Truth nữa, sẽ không biết là dữ liệu thực sự thì ở đâu. Việc này khiến cho chương trình lúc nào cũng phải
compute value on-the-fly
để đảm bảo dữ liệu không mất sự đồng bộ và gây nên vấn đề trong việc bảo trì.
Hãy xem lại ví dụ trên:
getInitialState() {
return {
liked: this.props.like
};
},
var AvatarEl = <Avatar
name="Foo"
width={100}
height={100}
src='http://canime.files.wordpress.com/2010/05/mask-dtb.jpg'
like={false}
/>;
Mặc dù tôi đã cố tình dùng
like
cho
props
và
liked
cho
state
, nhưng nó vẫn có khả năng nhầm lẫn.
Tuy nhiên, nó sẽ không phải là một
anti-pattern
nếu bạn làm rõ việc synchronization không phải là mục tiêu ở đây:
getInitialState() {
return {
liked: this.props.initialLike
};
},
var AvatarEl = <Avatar
name="Foo"
width={100}
height={100}
src='http://canime.files.wordpress.com/2010/05/mask-dtb.jpg'
initialLike={false}
Thao khảo thêm bài viết về điều này
tại đây
Using State in parent, Prop as I/F for children.
Qua các ví dụ trên, chúng ta đã có một Component Avatar có chứa các thuộc tính như name, width, height, src, like (button like thì thực ra chúng ta nên tách nó ra thành một Component hoàn chỉnh). Trong thực tế, thông thường chúng ta sẽ có một list các Component. Hãy xem thử cấu trúc sau:
- Avatar List
- Avatar
- name
- height
- width
- src
- initialLike
- Avatar
- name
- height
- width
- src
- initialLike
- Avatar
- name
- height
- width
- src
- initialLike
Ta xem Avatar List ở đây như làm một Component cha, còn Avatar là một Component con. Component cha sẽ truyền các giá trị vào Component con thông qua I/F
props
của Component con.
Chúng ta thử refactor code ở phần trước để minh họa cho điều này như sau:
<!DOCTYPE html>
<html>
<head>
<meta http-equiv='Content-type' content='text/html; charset=utf-8'>
<title>Basic Example Props</title>
</head>
<body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.13.3/react.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.13.3/JSXTransformer.js"></script>
<script type="text/jsx">
var Avatar = React.createClass({
propTypes: {
name: React.PropTypes.string.isRequired,
width: React.PropTypes.number.isRequired,
height: React.PropTypes.number.isRequired,
initialLike: React.PropTypes.bool.isRequired,
},
getInitialState() {
return {
liked: this.props.initialLike
};
},
onClick() {
this.setState({liked: !this.state.liked});
},
render() {
var textLike = this.state.liked ? 'like' : 'haven\'t liked';
return (
<li>
<img src={this.props.src} width={this.props.width} height={this.props.height} alt="alt" />
<span>{this.props.name}</span>
<button onClick={this.onClick}>{textLike}</button>
</li>
);
}
});
var Avatars = React.createClass({
getInitialState() {
return {
avatars: [
{name: "Avatar 1", height: 100, width: 100, initialLike: false, src: "http://canime.files.wordpress.com/2010/05/mask-dtb.jpg"},
{name: "Avatar 2", height: 100, width: 100, initialLike: true, src: "http://z4.ifrm.com/30544/116/0/a3359905/avatar-3359905.jpg"},
{name: "Avatar 3", height: 100, width: 100, initialLike: false, src: "http://www.dodaj.rs/f/O/IM/OxPONIh/134.jpg"}
]
}
},
render() {
var avatars = this.state.avatars.map(function(avatar){
var AvatarEl = <Avatar
name={avatar.name}
width={avatar.width}
height={avatar.height}
src={avatar.src}
initialLike={avatar.initialLike}
/>;
return AvatarEl;
});
return (
<ul>
{avatars}
</ul>
);
}
});
var AvatarsComponent = React.render(<Avatars />, document.body);
</script>
</body>
</html>
Vì để thấy được sự chuyển tiếp từ phần trước, cũng như cho mọi người dễ hình dung hơn về mối quan hệ cha-con, cho nên chuyển đoạn
var AvatarEl
ở ví dụ trước vào trong Component Avatars. Tuy nhiên trong thực tế thì phần source code đó, có thể viết gọn hơn như sau:
return <Avatar name={avatar.name} width={avatar.width} height={avatar.height} src={avatar.src}initialLike={avatar.initialLike} />;
Component cha handling event của Component con
Trong dạng list, chúng ta thường hay có các action như thêm item, sửa item, và xóa item. Bây giờ ta thử thêm chức năng xóa item. Khi xóa Component con thì list Component sẽ thay đổi, việc thay đổi này sẽ update lại list Component. Như vậy ở đây, chúng ta thấy một logic như sau:
- Click button delete trên Component con.
- Component con phát sự kiện delete lên Component cha.
- Component cha update lại list.
Source code:
<!DOCTYPE html>
<html>
<head>
<meta http-equiv='Content-type' content='text/html; charset=utf-8'>
<title>Basic Example Props</title>
</head>
<body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.13.3/react.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.13.3/JSXTransformer.js"></script>
<script type="text/jsx">
var Avatar = React.createClass({
propTypes: {
id: React.PropTypes.number.isRequired,
name: React.PropTypes.string.isRequired,
width: React.PropTypes.number.isRequired,
height: React.PropTypes.number.isRequired,
initialLike: React.PropTypes.bool.isRequired,
onDelete: React.PropTypes.func.isRequired,
},
getInitialState() {
return {
liked: this.props.initialLike
};
},
onClick() {
this.setState({liked: !this.state.liked});
},
_onDelete() {
this.props.onDelete(this.props.id);
},
render() {
var textLike = this.state.liked ? 'like' : 'haven\'t liked';
return (
<li key={this.props.id}>
<span>{this.props.id}</span>
<img src={this.props.src} width={this.props.width} height={this.props.height} alt="alt" />
<span>{this.props.name}</span>
<button onClick={this.onClick}>{textLike}</button>
<button onClick={this._onDelete}>Delete</button>
</li>
);
}
});
var Avatars = React.createClass({
getInitialState() {
return {
avatars: [
{id: 1, name: "Avatar 1", height: 100, width: 100, initialLike: false, src: "http://canime.files.wordpress.com/2010/05/mask-dtb.jpg"},
{id: 2, name: "Avatar 2", height: 100, width: 100, initialLike: true, src: "http://z4.ifrm.com/30544/116/0/a3359905/avatar-3359905.jpg"},
{id: 3, name: "Avatar 3", height: 100, width: 100, initialLike: false, src: "http://www.dodaj.rs/f/O/IM/OxPONIh/134.jpg"}
]
}
},
deleteItem(id) {
this.setState({
avatars: this.state.avatars.filter(function(avatar){
return avatar.id !== id;
})
});
},
render() {
var avatars = this.state.avatars.map(function(avatar){
return <Avatar onDelete={this.deleteItem} id={avatar.id} name={avatar.name} width={avatar.width} height={avatar.height} src={avatar.src} initialLike={avatar.initialLike} />;
}, this);
return (
<ul>
{avatars}
</ul>
);
}
});
var AvatarsComponent = React.render(<Avatars />, document.body);
</script>
</body>
</html>
Hình bên dưới sẽ giúp bạn hiểu rõ hơn về mối quan hệ giữa event của Component con và Component cha.
Khi user click vào button Delete, gọi đến
onDelete
method Interface đã được binding với method
deleteItem()
ở Component cha. Việc update lại list Component sẽ được xử lý trong method này.