今日はこれまでに紹介したPropやStateを使ったComponent間でやりとりについて書きたいと思います。
親のStateを子のPropとして渡す
Componentを設計する時はまずPropとしてI/Fを考えて、そのComponentが管理すべき値で変更されるものをStateとして定義します。
つまりComponent間での親子の関係を意識して、親がStateを持っていて子にPropとして渡すというのが基本になります。(子は使うだけで管理しているのは親)
var User = React.createClass({
propTypes: {
name: React.PropTypes.string.isRequired,
id: React.PropTypes.number.isRequired
},
render() {
return (
<div>{this.props.id}:{this.props.name}</div>
);
}
});
var request = require('superagent');
var Users = React.createClass({
getInitialState() {
return {
users: [ {id: 1, name: "foo"}, {id: 2, name: "bar"} ]
}
},
componentDidMount() {
request.get('http://example.com/users/', (res) => {
this.setState({users: res.body.users});
});
},
render() {
var users = this.state.users.map((user) => {
return <User id={user.id} name={user.name} key={user.id}/>
});
return (
<div>
<p>ユーザー一覧</p>
{users}
</div>
);
}
});
子でのイベントを親でハンドリングする
子のComponentの中で発生するイベントで親がハンドリングしたい場合は、子がハンドリングするための関数をPropでI/Fとして公開し、親がそこに処理を渡す形になります。
TodoListで各Todoが子のComponentになっていて子のComponentに削除や編集のUIがある時に、削除・編集処理はTodoListのComponentに定義しておいて各TodoのComponentから委譲されるイメージです。
var Todo = React.createClass({
propTypes: {
todo: React.PropTypes.shape({
id: React.PropTypes.number.isRequired,
text: React.PropTypes.string.isRequired
}),
// 削除するための処理をI/Fとして定義
onDelete: React.PropTypes.func.isRequired
},
// 親に処理を委譲する
_onDelete() {
this.props.onDelete(this.props.todo.id);
},
render() {
return (
<div>
<span>{this.props.todo.text}</span>
<button onClick={this._onDelete}>delete</button>
</div>
);
}
});
var TodoList = React.createClass({
getInitialState() {
return {
todos: [
{id:1, text:"advent calendar1"},
{id:2, text:"advent calendar2"},
{id:3, text:"advent calendar3"}
]
};
},
// TodoListはこのComponentが管理しているので削除する処理もここにあるべき
deleteTodo(id) {
this.setState({
todos: this.state.todos.filter((todo) => {
return todo.id !== id;
})
});
},
render() {
var todos = this.state.todos.map((todo) => {
return <li key={todo.id}><Todo onDelete={this.deleteTodo} todo={todo} /></li>;
});
return <ul>{todos}</ul>;
}
});
React.render(<TodoList />, document.body);
PropでStateの初期値を渡す
また、PropとしてStateの初期値を渡したいような場面があるかもしれません。
var Counter = React.createClass({
propTypes: {
count: React.PropTypes.number
},
getDefaultProps() {
return {
count: 0
};
},
getInitialState() {
return {
count: this.props.count
}
},
onClick() {
this.setState({ count: this.state.count + 1 });
},
render() {
return (
<div>
<p>{this.state.count}</p>
<button onClick={this.onClick}>click</button>
</div>
);
}
});
//<Counter count=10 />
その時に↑のような形にしてしまうと、Propのcountも一緒に増加していきそうに感じさせてしまいます。
なので初期値として渡したい時は、Propの名前を初期値だと明確にわかるような名前にすべきです。
var Counter = React.createClass({
propTypes: {
initialCount: React.PropTypes.number
},
getDefaultProps() {
return {
initialCount: 0
};
},
getInitialState() {
return {
count: this.props.initialCount
}
},
onClick() {
this.setState({ count: this.state.count + 1 });
},
render() {
return (
<div>
<p>{this.state.count}</p>
<button onClick={this.onClick}>click</button>
</div>
);
}
:
});
//<Counter initialCount=10 />
ref
refをComponentに対して指定することで、this.refs.xxx
という形で子のComponentに対する参照を取得することが出来ます。これを使うことで親から子のメソッドが呼べたりするのですが、これをやりだすとComponent間の関係がわかりにくくなるので、基本的にはrefはdiv
やbutton
など組み込みのComponentに対して使うものだと考えておくといいのかなと思います。
また、後述のgetDOMNode
と一緒に使う場面が多いのでセットで考えておいてもいいかと思います。
var Test = React.createClass({
componentDidMount() {
console.log(this.refs.myDiv.props.children); // xxx
},
render() {
return (
<div ref="myDiv">xxx</div>
);
}
});
getDOMNode
React.jsではDOM操作はVirtualDOMに隠蔽されているので、直接DOM操作をすることはありません。
しかしながら、focus
を合わせたりjQuery Pluginなんかを使ったりするときに直接DOMを触りたいことがあるかもしれません。
そういったときにはref
とセットでgetDOMNode
を使うとDOMへの参照を取得することが出来ます。
ただ、DOMを直接変えてしまうとVirtualDOMとの整合性が崩れるので読み取りだけにしたほうがいいです。
var Focus = React.createClass({
componentDidMount() {
this.refs.myText.getDOMNode().focus();
},
render() {
return (
<div>
<p>set focus</p>
<input type="text" ref="myText" />
</div>
);
}
});
props.children
<myComponent>xxx</myComponent>
のように書いたときにxxxを取得したい場合には、this.props.children
で取得することが出来ます。
var Hello = React.createClass({
render() {
return <div>{this.props.children}</div>;
}
});
console.log(
React.render(
<Hello>xxx</Hello>,
document.body
).props.children
);
// => xxx
console.log(
React.render(
<Hello><span>1</span><span>2</span></Hello>,
document.body
).props.children
);
// => [React.Element, React.Element]
console.log(
React.render(
<Hello></Hello>,
document.body
).props.children
);
// undefined
上記のようにprops.children
はただの文字列だったり、React Elementの配列だったりundefinedだったりと指定のされ方によって様々です。
なので例えば配列だと思って数を確認するためにchildren.lengthした場合、文字列が指定されるとString.lengthが返ってきてしまいます。
そのためchildrenを利用する際にはchildrenが何であるか確認する必要が出て来てしまいます。
そういった場合に、React.Children
にcount
、forEach
、map
、only
という便利な関数が提供されているのでそれを使うとその辺りの違いを吸収してくれます。
var Hello = React.createClass({
render() {
return <div>{this.props.children}</div>;
}
});
[
<Hello>xxx</Hello>,
<Hello><span>1</span><span>2</span></Hello>,
<Hello></Hello>
].forEach( jsx => {
var children = React.render(jsx, document.body).props.children;
console.log("#########" + children + "##########");
console.log(React.Children.count(children));
React.Children.forEach(children, (child) => { console.log(child) });
});
// #########xxx##########
// 1
// xxx
// #########[object Object],[object Object]##########
// 2
// ReactElement {type: "span", key: null, ref: null, _owner: null, _context: Object…}
// ReactElement {type: "span", key: null, ref: null, _owner: null, _context: Object…}
// #########undefined##########
上のように配列でも文字列でも違いを吸収してくれています。
ちなみにReact.Children.only
はchildrenが1つのReact.element以外の時にエラーを投げる関数です。
というわけで今回はComponent間のやりとりについて書きました。
明日はComponentのLifecycleについて書きたいと思います。