reactjs

React.jsでPropやStateを使ってComponent間のやりとりをする

More than 3 years have passed since last update.

今日はこれまでに紹介した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はdivbuttonなど組み込みの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.ChildrencountforEachmaponlyという便利な関数が提供されているのでそれを使うとその辺りの違いを吸収してくれます。

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について書きたいと思います。