はじめに
React 初心者が、コンポーネント間の情報をやりとりするには、どうすれば良いのか、自分なりに考えた結果を整理しています。
間違ったコードの例を先に書いてますので、間違ったコードをすっ飛ばして、正しい(と私が考える)コードのを知りたい方は最後の方だけ呼んでください。
なお、Redux とか Flux とかはまだ勉強してないので、そちらは扱いません。
Reactの考え方
基本的に、以下の考え方に落ち着きました。
- 親コンポーネントから子コンポーネントに情報を渡すには property を使って情報を渡す。
- 子コンポーネントから親コンポーネントに情報を渡すには、親から関数を property で渡し、子からは親の関数を呼び出すことで情報を渡す。
- 兄弟コンポーネントで情報を渡すには親コンポーネントを経由する。
- Reactがうまいことやってくれるので、自分で直接コンポーネント間で情報を渡そうと考えない(情報を渡すコードを書かない)。
- オブジェクト指向的な考え方は邪魔になることがある。
例題
例題として以下のようにテキストフィールドに何か入力すると、その内容をリアルタイムに表示するプログラムを作ってみます。
間違った考え方
上記の基本的な考え方に至る前の間違った(と私が判断した)考え方で、あえて実装してみましょう。
コンポーネントを考える
React を使う場合、コンポーネントを作って、それらを組み合わせることで画面を作っていきます。
今回は、テキスト入力のコンポーネント、入力された内容を表示するメッセージのコンポーネント、この2つをまとめる全体のコンポーネントを作ることにします。
(ここまでは正しい。)
テキスト入力のコンポーネントでは何が入力されているか、メッセージのコンポーネントでは何を表示すれば良いかの status を保持するようにします。
(← これが間違い。オブジェクト指向だとそれぞれのコンポーネントがそれぞれ自分の情報を保持しているという考え方になります。ですが、この後、進めていくとわかる通り、React で、この考え方に引きずられるとうまく行きません。)
コンポーネントを作る
まずはテキスト入力のコンポーネントを作ります。( 実装自体はまずくないですが、間違った考え方に基いているので、今回の場合は、この実装もよろしくありません。 )
class Textinput extends React.Component {
constructor(props) {
super(props);
this.state = {
value: ""
};
this.handleTextChanged = this.handleTextChanged.bind(this);
}
handleTextChanged(event) {
this.setState({
value: event.target.value
});
}
render() {
return (
<div>
<label htmlFor="value">何か入力してください</label>
<input type="text" id="value" onChange={this.handleTextChanged} />
</div>
);
}
}
次にメッセージのコンポーネントを作ります。
(これも間違った考え方から実装しているので不適切です。)
class Message extends React.Component {
constructor(props) {
super(props);
this.state = {
value: ""
};
}
render() {
return (
<div>
<p>
「{this.state.value}」と入力されました
</p>
</div>
);
}
}
status を変更する処理が無いのですが、それは後から考えます。(←これも間違い。「変更する処理」という発想が間違っている。)
入力コンポーネントとメッセージコンポーネントを保持する全体のコンポーネントを作ります。
class App extends React.Component {
constructor(props) {
super(props);
}
render() {
return (
<div>
<p>React version {React.version}</p>
<Textinput />
<Message />
</div>
);
}
}
最後は、ReactDOM.render で表示します。
ReactDOM.render(<App />, document.getElementById("root"));
index.html の方はこんな感じです。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<script type="text/babel" src="index.js"></script>
<script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
<title></title>
</head>
<body>
<dev id="root"></dev>
</body>
</html>
兄弟同士で情報をやりとりする
入力コンポーネントで内容が変更されたら、それをメッセージコンポーネントへ伝えたいのです。
ですが、直接、伝える手段を見つけられなかったので
入力コンポーネント → 親コンポーネント → メッセージコンポーネント
と親を介して情報を伝えます。
子から親へ情報を伝える
子から親へ情報を伝えるためには、親で関数を作成し、親の関数を子コンポーネント(入力コンポーネント)にpropertyとして渡します。
子コンポーネントでは、親コンポーネントの関数を呼び出すことで情報を親に伝えます。(これは正しい)
親コンポーネント側は status を持つようにし、そのstatusを更新する関数を追加、その関数を入力コンポーネントに渡すようにします。(これも正しい)
class App extends React.Component {
constructor(props) {
super(props);
this.state = {value: ""} // これを追加
this.setValue = this.setValue.bind(this); //これを追加
}
// この関数を追加
setValue(value) {
this.setState({
value: value
});
}
// Textinput コンポーネントに setter で関数を渡す
render() {
return (
<div>
<p>React version {React.version}</p>
<Textinput setter={this.setValue}/>
<Message />
</div>
);
}
}
子コンポーネントである Textinput コンポーネントでは、親の関数を呼び出すようにします。(←修正するところまでは良いのですが、不要なコードが残ってます。)
class Textinput extends React.Component {
constructor(props) {
super(props);
this.state = { value: "" };
this.handleTextChanged = this.handleTextChanged.bind(this);
}
handleTextChanged(event) {
this.setState({
value: event.target.value
});
this.props.setter(event.target.value); // これを追加します。
}
render() {
return (
<div>
<label htmlFor="value">何か入力してください</label>
<input type="text" id="value" onChange={this.handleTextChanged} />
</div>
);
}
}
親コンポーネントから子コンポーネントへ情報を伝える
この時点で、入力テキストの内容を修正すると、親のstatusが変わるようになっています。
あとは、親の status の変更が子コンポーネント(メッセージコンポーネント)に伝えるだけです。
親コンポーネントから、子コンポーネントに情報を伝えるには、props を使います。
class App extends React.Component {
constructor(props) {
super(props);
this.state = {value: ""}
this.setValue = this.setValue.bind(this);
}
setValue(value) {
this.setState({
value: value
});
}
// Message に value property を渡します。
render() {
return (
<div>
<React.StrictMode>
<p>React version {React.version}</p>
<Textinput setter={this.setValue}/>
<Message value={this.state.value}/>
</React.StrictMode>
</div>
);
}
}
Message コンポーネントでは、value property で渡された値をstatusに設定します。
(← これが間違い。)
class Message extends React.Component {
constructor(props) {
super(props);
this.state = { value: props.value }; // この行を追加します。
}
render() {
return (
<div>
<p>
「{this.state.value}」と入力されました
</p>
</div>
)
}
}
これだけだと動作しません。propsの値が変更された時に Message コンポーネントの status を変更する必要があります。
そこで、 componentWillReceiveProps
を使います。(← これも間違い)
class Message extends React.Component {
constructor(props) {
super(props);
this.state = {
value: props.value
};
}
// これを追加して status を変更する。
componentWillReceiveProps(props) {
this.setState({
value: props.value
})
}
render() {
return (
<div>
<p>
「{this.state.value}」と入力されました
</p>
</div>
)
}
}
これで、テキストフィールドの値を変更すると、メッセージも変わるようになりました。(←繰り返しますが、これは間違った実装です。)
何が間違っているのか
この実装の何が間違っているのでしょうか?
- それぞれのコンポーネントで結局、同じ status を保持している。
- 推奨されない関数 componentWillReceiveProps を使っている。
- Message コンポーネントの status を直接、 componentWillReceiveProps を使って setStatus で明示的に変更しようとしている。
正しい(と私が考える)実装
- status は親コンポーネントだけが持つようにします。
- 推奨されない関数 componentWillReceiveProps を使わないようにします。
- status を直接、明示的に変更するという考え方をやめる。
下の2つは、React は、
- 子から親へ情報を伝えるには、親からプロパティで渡された関数を使う。
- 親から子へ情報を伝えるには、プロパティで渡す。
- 兄弟同士では直接情報をやりとりせず、親を経由してやりとりする。
- 親から子へプロパティで渡された情報は、React がうまいこと、更新されたことを伝えてくれる。
という仕組みになっていると私が考えていることに基づいています。
入力コンポーネントの実装
入力コンポーネントでは、status を持たず、onChange プロパティで直接、親から渡された関数を呼び出すようにします。
classにするのもやめて関数として実装します。
const Textinput = props => (
<div>
<label htmlFor="value">何か入力してください</label>
<input type="text" id="value" onChange={props.handleChanged} />
</div>
);
メッセージコンポーネントの実装
メッセージコンポーネントも、status を持たないようにします。直接、親コンポーネントから渡されたプロパティの情報を表示するようにします。
こちらも class にするのをやめにします。
const Message = props => (
<div>
<p>「{props.value}」と入力されました</p>
</div>
);
最終的な実装
親コンポーネントから入力コンポーネントに渡す関数の引数が値から、イベントに変わるのでそこを修正します。(関数名も修正します。)
最終的な実装は以下のようになります。
子コンポーネント側には、 status が存在しないこと、子コンポーネントが setStatus を呼び出していないこと、
表示コンポーネントの props.value の値を直接、変更するようなコードが、存在しないことに注意してください。
const Textinput = props => (
<div>
<label htmlFor="value">何か入力してください</label>
<input type="text" id="value" onChange={props.handleChanged} />
</div>
);
const Message = props => (
<div>
<p>「{props.value}」と入力されました</p>
</div>
);
class App extends React.Component {
constructor(props) {
super(props);
this.state = {value: ""}
this.handleChanged = this.handleChanged.bind(this);
}
// 引数が event に変わるので、ここを修正する。
handleChanged(event) {
this.setState({
value: event.target.value
});
}
render() {
return (
<div>
<React.StrictMode>
<p>React version {React.version}</p>
<Textinput handleChanged={this.handleChanged}/>
<Message value={this.state.value}/>
</React.StrictMode>
</div>
);
}
}
ReactDOM.render(
<App />,
document.getElementById('root')
);
まとめ
繰り返しになりますが、少し、Reactを触ってみて理解できたことは、次の内容です。
- 親コンポーネントから子コンポーネントに情報を渡すには property を使って情報を渡す。
- 子コンポーネントから親コンポーネントに情報を渡すには、親から関数を property で渡し、子からは親の関数を呼び出すことで情報を渡す。
- 兄弟コンポーネントで情報を渡すには親コンポーネントを経由する。
- Reactがうまいことやってくれるので、自分で直接コンポーネント間で情報を渡そうと考えない(情報を渡すコードを書かない)。
- オブジェクト指向的な考え方は邪魔になることがある。
ここまでの考えに至るまでにやったことは次の通りです。
- Hello World 以下の MAIN CONCEPTS を順番に読んで写経する。
- Accessibility 以下の ADVANCED GUIDES を順番に読んで、写経できるものは、写経する。
- 自分で React を使った簡単なプログラム を、写経ではなく、自力で(2つ3つ)書いてみる。
自分では、オフィシャルサイトのドキュメント の内容を消化しきれていないと思います。
でも、React を触るのであれば、一度くらいは、 MAIN CONCEPTS と ADVANCED GUIDES を一通り、読んでみるのが良いと思いました。