LoginSignup
2
2

More than 5 years have passed since last update.

React-Redux超入門(2)

Last updated at Posted at 2018-08-02

概要

前回の記事をベースに,コードを整理すること,またコンポーネント間で値を受け渡すいくつかのやり方をまとめる.ここでは,

  • テキストフィールド1つ
  • 登録ボタン1つ
  • 登録一覧

の要素を備え,テキストフィールドに文字を入力し,登録ボタンを押すと登録一覧が更新されるアプリケーションを例題にする.
今回は1ファイルではなく,複数ファイルでアプリケーションを構成する.

方針1

方針1でのコンポーネントとファイルの関係は以下の通り.

  • App.js: アプリケーションのメインファイル.Appコンポーネントを含む
  • Input.js: テキストフィールドコンポーネントを含む
  • Item.js: 追加される要素1つを表す
Item.js
import React from 'react'

const Item = (props) => {
    return (
        <li>{props.name}</li>
    )
}
export default Item;

Itemは関数コンポーネントで定義する.表示する内容はProps経由で受け取る.

Input.js
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)は使えないためである.

App.js
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と変更なし
Input2.js
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は,inputbuttonを内包し,buttononClickイベントで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と変更なし
Input3.js
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イベントハンドラも設定しないと警告が出る.

ItemList.js
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で行っていたループ処理の代わりの関数コンポーネント.

App.js
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;

単にItemListInput3を追加し,Input2をコメントアウト.

以上,どの方法でも動作するが,React的には方針3が良いのだと思われる.

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2