概要
前回の記事をベースに,コードを整理すること,またコンポーネント間で値を受け渡すいくつかのやり方をまとめる.ここでは,
- テキストフィールド1つ
- 登録ボタン1つ
- 登録一覧
の要素を備え,テキストフィールドに文字を入力し,登録ボタンを押すと登録一覧が更新されるアプリケーションを例題にする.
今回は1ファイルではなく,複数ファイルでアプリケーションを構成する.
方針1
方針1でのコンポーネントとファイルの関係は以下の通り.
- App.js: アプリケーションのメインファイル.Appコンポーネントを含む
- Input.js: テキストフィールドコンポーネントを含む
- Item.js: 追加される要素1つを表す
import React from 'react'
const Item = (props) => {
return (
<li>{props.name}</li>
)
}
export default Item;
Itemは関数コンポーネントで定義する.表示する内容はProps経由で受け取る.
import React, {Component} from 'react'
class Input extends Component {
render() {
return <input type="text" ref="ff" defaultValue="Input Todo"/>
}
}
export default Input;
Inputコンポーネントはクラスコンポーネントで定義する.方針1では,親コンポーネント(App)からInputコンポーネントに入力された値を参照する.そのため,ref(s)
を使う必要があるが,関数コンポーネントにはこのref(s)
は使えないためである.
import React, { Component } from 'react';
import './App.css';
import Item from './Item'; // (1) コンポーネントのインポート
import Input from './Input';
class App extends Component {
constructor(props) {
super(props);
this.state = {
items: []
}
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
const {items} = this.state;
items.push(this.refs.iform.refs.ff.value); // (2)refsを使用した子(孫)コンポーネントの参照
this.setState({items});
}
render() {
return (
<div>
<h1>Todo App</h1>
<Input ref="iform"/>
<button onClick={this.handleClick}>Add</button> // (3) ボタンとイベントハンドラの設定
<ul>
{
(() => {
let list = [];
const { items } = this.state;
for (var i = 0; i < items.length; ++i) {
list.push(<Item key = {i} name={items[i]} />);
}
console.log(list);
return list;
})()
}
</ul>
</div>
);
}
}
export default App;
Appはcreate-react-app
で生成されるものを修正して作成した.ポイントは3つ.その他は前回説明ずみ.
(1)
自作コンポーネントのインポート.ここでは同じディレクトリにインポートするファイルが存在することを仮定している.
(2)
イベントハンドラでは,ステートで管理されているitems
を取り出し,Inputコンポーネントに入力された文字列を追加してステートを更新する.方針1では,コンポーネントの値をDOMを経由して取得している.Reactは仮想DOMという単位でコンポーネントを管理しているらしく,ブラウザが管理している生のDOMを参照するには,ref(s)
という仕組みを使う必要があるらしい.そこで,参照されるオブジェクトを一意に定めるため,ref
を使って名前をつけている.この例では,Input
コンポーネントにref="iform"
で名前をつけ,さらにInput
コンポーネントが内包するinput
コンポーネントにもref="ff"
という名前をつけている.そのため,Appからff
のvalueのアクセスするには,
this.refs.iform.refs.ff.value
と書く必要がある.
(3)
方針1では,ボタンは<button>
をそのまま利用し,onClick
で(2)のイベントハンドラを関連付けている.
このアプリケーションを実行すると,Addボタンを押すたびにリストに追加されていく.
このアプリケーションは動作が,この方針1はよろしくない.なぜならば,Appが孫コンポーネントまで知る必要があるからである.もし,Inputコンポーネントが内包するinput
の名前が変わったり,使うコンポーネントが変わったりしたらその都度Appも変更しなければいけない.そのため,Addボタンが押されたら,Inputから通知を受け取り,その引数で登録する値を取得したいものである.
方針2
方針2でのコンポーネントとファイルの関係は以下の通り.
- App.js: アプリケーションのメインファイル.Appコンポーネントを含む.Input2を利用するため方針1から修正.
- Input2.js: テキストフィールドとボタンコンポーネントを含む.
- Item.js: 追加される要素1つを表す.方針1と変更なし
import React, {Component} from 'react'
class Input2 extends Component {
constructor(props) {
super(props);
this.addItem = this.addItem.bind(this);
}
addItem() {
this.props.addItem(this.refs.ff.value); //(1) onClickで呼ばれ,親から渡された関数を実行する.
}
render() {
return (
<div>
<input type="text" ref="ff" defaultValue="Input Todo"/>
<button onClick={this.addItem}>Add</button>
</div>
)
}
}
export default Input2;
(1)
Input2
は,input
とbutton
を内包し,button
のonClick
イベントでinput
に入力された文字列を取得してprops
で渡された関数(addItem
)を実行する.
:App.js
import React, { Component } from 'react';
import './App.css';
import Item from './Item';
import Input from './Input';
import Input2 from './Input2';
class App extends Component {
constructor(props) {
super(props);
this.state = {
items: []
}
this.handleClick = this.handleClick.bind(this);
this.addItem = this.addItem.bind(this);
}
handleClick() {
const {items} = this.state;
items.push(this.refs.iform.refs.ff.value);
this.setState({items});
console.log(this.refs.iform.refs.ff.value);
}
addItem(v) {
const {items} = this.state;
items.push(v);
this.setState({items});
}
render() {
return (
<div>
<h1>Todo App</h1>
{
// <Input ref="iform"/> (1)Input, buttonを削除
// <button onClick={this.handleClick}>Add</button>
}
<Input2 addItem={this.addItem}/> (2) Input2を追加
<ul>
{
(() => {
let list = [];
const { items } = this.state;
for (var i = 0; i < items.length; ++i) {
list.push(<Item key = {i} name={items[i]} />);
}
console.log(list);
return list;
})()
}
</ul>
</div>
);
}
}
export default App;
変更点は(1), (2)の2箇所.
(1)
Input2のために不要になるInput
, button
の削除
(2)
Input2
を追加.PropsでApp
の関数を"addItem"という名前で渡している.こうすることで,Input2
ではこの関数をProps経由で実行できるようになる.
方針2では,方針1でやりたかったことが実現できている.すなわち,コールバックを作成(this.addItem
)し,下位コンポーネントに登録することで,下位からの通知を受け取る.この時,下位コンポーネントからは上位コンポーネントが必要な情報を引数で受け取っている.
方針2でもまあ,モジュール分割がうまくできるとは言える.でも,どうやらReactではref(s)
でHTMLのDOMにアクセスするのはできるだけ避けたほうが望ましいらしい.
方針3
方針2を踏まえ,ref(s)
を使わないように変更する.具体的には,ステートを使って値の取得を行う.
- ItemList.js: Itemをリストで返すコンポーネント.Appのループ処理をコンポーネント化する
- App.js: アプリケーションのメインファイル.Appコンポーネントを含む.ItemList, Input3を利用するため方針2から軽微な修正.
- Input3.js: テキストフィールドとボタンコンポーネントを含み,ステートで入力データを管理する
- Item.js: 追加される要素1つを表す.方針1,2と変更なし
import React, {Component} from 'react';
class Input3 extends Component {
constructor(props) {
super(props);
this.state = { // (1) ステートの設定
value: "input something",
}
this.addItem = this.addItem.bind(this);
this.handleChange = this.handleChange.bind(this);
}
addItem() {
this.props.addItem(this.state.value); // (2) ボタンのイベントハンドラ
}
handleChange(e) {
this.setState({...this.state, value:e.target.value}); //(3) テキストボックスのイベントハンドラ
}
render() {
return (
<div>
<input type="text" onChange={this.handleChange} value={this.state.value}/> (4)
<button onClick={this.addItem}>Add</button>
</div>
)
}
}
export default Input3;
要点な4つ.
(1)
後述するテキストボックスの値を管理するためのステートを生成/初期化.
(2)
ボタンのイベントハンドラ.テキストボックスの値をステートで管理するので,this.state.value
から値を取り出して親のaddItem
に与えている.
(3)
テキストボックスのイベントハンドラ.テキストボックスに変化がある度(e.g, 文字の入力など)に実行される.ここでは引数e
から値を取り出してthis.state.value
に設定する.
(4)
テキストボックスの生成.onChange
イベントにイベントハンドラを設定し,同時にvalue
にステートで管理する値(this.state.value
)をバインドしている.なお,テキストボックスでは,value
を設定する時には同時にonChange
イベントハンドラも設定しないと警告が出る.
import React from 'react';
import Item from './Item';
const ItemList = ({items}) => {
let list = [];
for (var i = 0; i < items.length; ++i) {
list.push(<Item key = {i} name={items[i]} />);
}
return list;
}
export default ItemList;
App
で行っていたループ処理の代わりの関数コンポーネント.
mport React, { Component } from 'react';
import './App.css';
import Input3 from './Input3';
import ItemList from './ItemList';
class App extends Component {
constructor(props) {
super(props);
this.state = {
items: []
}
this.handleClick = this.handleClick.bind(this);
this.addItem = this.addItem.bind(this);
}
handleClick() {
const {items} = this.state;
items.push(this.refs.iform.refs.ff.value);
this.setState({items});
console.log(this.refs.iform.refs.ff.value);
}
addItem(v) {
const {items} = this.state;
items.push(v);
this.setState({items});
}
render() {
return (
<div>
<h1>Todo App</h1>
<Input3 addItem={this.addItem}/>
<ul>
<ItemList items={this.state.items} />
</ul>
</div>
);
}
}
export default App;
単にItemList
とInput3
を追加し,Input2
をコメントアウト.
以上,どの方法でも動作するが,React的には方針3が良いのだと思われる.