しかたらむーの てっくぶろ

組み込みソフトの古参エンジニアがモダンなフロントエンドエンジニアに転生 その進化の記録です

『レスポンシブでスクローラブルなリスト』を React で

はじめに

前回、Reactをやってみるってことで3ステップで環境構築してみました。
今回はこれをベースにして、『スクローラブルリスト』をReact でやってみて Vueとの違いを理解してみたいと思います。

出来上がりはこんな感じです。

Vue とあまり変わり映えしませんが、出来上がりはこんな感じです。
一応、React のロゴになってるのわかりますでしょうか?

f:id:Shikataramuno:20190324134106p:plain

コードはここです。
GitHub - Shikataramuno/responsive-scrollable-grid-by-react: レスポンシブでスクロール可能なリスト 今度はReact

React でやることを優先したので、Typescript ではなく敢えてJavaScript でやってみました。

`React'の詳しい仕様はここを参照してください。
React – A JavaScript library for building user interfaces

MemberList コンポーネントの追加

まず、前回の記事を参考に test-project ってプロジェクトを作っておいてください。
src フォルダ以下に MemberList.js ってファイルを作ってコンポーネントを定義します。コンポーネントのインターフェースは以下の様にします。

import React, { Component } from 'react';
import {Row, Col, Form, ProgressBar} from 'react-bootstrap';
import 'bootstrap/dist/css/bootstrap.min.css';
import './MemberList.css';

export default class MemberList extends Component {

  columns = ['id', 'name', 'admin', 'address', 'progress'];
  constructor(props) {...}

  filter = (e) => {...}
  sortBy = (name) => {...}
  handleAdminChanged = (member) => {...}

  dhead = () => {...}
  dlist = () => {...}
  list = () => {...}
  render () {    return <this.list/>  }
  componentDidMount () {...}
}

9行目は、MemberList クラスのコンストラクタです。
React では、コンストラクタでコンポーネントの状態にはどんな項目があるかを定義します。MemberList クラスでは、ソートキー、ソートオーダー、フィルタ文字列、メンバーのリストなんかをコンポーネントの状態として定義しておきます。

11行目~13行目はUIエレメントのイベントハンドラの定義です。
MemberListコンポーネントでは、リストフィルタのテキスト入力イベント、リストソートのタイトルクリックイベント、リストメンバのチェックボックスのチェンジイベントの3つのイベントハンドラを定義します。
何れも Arrow 関数で宣言し、暗黙的に this 参照を Componentbind する様にしています。

15行目~18行目の renderまでがHTML を生成するコードに該当します。
リストのヘッダ部分の htmldhead() メソッドで、リスト部分の htmldlist() メソッドで作成します。そして、それらの html コードを list() メソッドで統合します。これを render() メソッドから呼び出す構造にしています。

19行目からはコンポーネントLifeCycle メソッドです。Vueで言うところの createdmountedに該当するメソッドです。 今回はマウントされた時点でコンポーネントの状態を初期化する様にするため componentDidMount() メソッドで初期化を実行します。

では、それぞれについて、もう少し詳しく見ていきましょう

コンストラク

コンストラクタの実装は次のとおりです。

constructor(props) {
  super(props);
  this.state = {
    members: [],
    sortOrders: {},
    sortKey: ""
  }
 }

UIの操作や、外部との通信によって変化する情報があれば、それをコンポーネントの状態はthis.stateで定義しておききます。
MemberListコンポーネントでは状態にメンバー、ソートオーダー、そートーキーを定義し、それぞれ[],{},''で初期化しています。

フィルタフィルタのテキスト入力イベント

  filter = (e) => {
    const filter = e.target.value;
    let members = this.memberList;
    members = members.filter((member) => {
      return Object.keys(member).some((key) => {
        return String(member[key]).indexOf(filter) > -1;
      });
    });
    this.setState({members: members})
  }

入力された文字列でリストをフィルタするので、filterという名前でArrow関数を使って定義しています。これでデフォルトでthisbindされます。

2行目で引数で受け取ったイベントオブジェクトからイベントを検知したUIの要素を抽出しています。3~8行目の内容はVueと何ら変わりません。

Vueと異なるのは、9行目でフィルタした結果で明示的にコンポーネントの状態を更新しているところです(this.setState)。 この更新操作によりコンポーネントの状態が更新され、それを契機にrender関数が呼ばれUIが再描画される一連の処理がリアクティブに動きます。

Vueではコンポーネントに閉じた状態であれば、直接アクセスして更新できましたがReactでは必ずthis.setState()で更新しなければなりません。React厳格に一通のデータフローに従うポリシーなのです。

dhead = () => {
  return (
    <div className="pc table-row header">
      <Row>
        <Form.Label className="title" >メンバ一覧 </Form.Label>
      </Row>
      <Row className='query-box'>
        <Col xs={2}>
          <Form.Control as="input" type="text" id="search" className="filter" placeholder="フィルタ文字列"
            onChange={this.filter}/>
        </Col>
        <Col xs={10}></Col>
      </Row>
      <div className="wrapper attributes header">
        ... 中略 ...
      </div>
    </div>
  );   
}

テキスト入力イベントはヘッダ部のForm要素で検知します。 10行目でFrom.ControlonChange イベントでfilter()メソッドを呼び出します。

リストソートのタイトルクリックイベント

  sortBy = (name) => {
    let sortOrders = this.state.sortOrders
    sortOrders[name] = sortOrders[name] * -1;
    let members = this.state.members;
    members = members.slice().sort((a,b) => {
      a = a[name]
      b = b[name]
      return (a === b ? 0 : a > b ? 1 : -1) * sortOrders[name];
    });
    this.setState({sortKey: name});
    this.setState({sortOrders: sortOrders});
    this.setState({members: members})
  }

このハンドラではイベントを検知した要素を知らなくてもよいので引数にイベントオブジェクトはありません。その代わり、イベントを検知したタイトル列の名前が引数で受け取る様にしています。

その他はテキスト入力イベントと同様です。

  dhead = () => {
    return (
      <div className="pc table-row header">
        <Row>
         ... 中略 ...
        </Row>
        <div className="wrapper attributes header">
          {
            this.columns.map((name,col) => {
              const className = this.state.sortKey === name ? "active " + name : name
              const arrow =
              this.state.sortKey === name ? 
                (this.state.sortOrders[name] > 0? <span className={"arrow asc"}></span> : <span className="arrow dsc"></span>) :
                "";
              return (
                <div className={className} key={col}
                  onClick={() => this.sortBy(name)}>
                  {name}
                  {arrow}
                </div>
              )
            })
          }
        </div>
      </div>
    );   
  }

sortBy()の呼び出しは、filter()と同じヘッダ部から呼び出されます。17行目が呼び出しで、引数に9行目の this.columns.map の処理内で個々の列名を示すnameを渡しています。

今回は、イベントを検知したUIの要素を特定する必要がないので引数にイベントオブジェクトを含めていませんが、イベントオブジェクトが必要でかつ、Arrow関数で定義している場合はイベントオブジェクトを明示的に引数に含めてやんなきゃいけません。 17行目はこんな感じです。

   onClick={(e) => this.sortBy(name, e)}>

チェックボックスイベントハンドラ

`sortBy()'と同じなので割愛しまぁ~す (謝)

index.jsを変更する

あとは、index.jsで 'App' コンポーネントを呼び出すようになっているところを MeberList コンポーネントに変更します。4行目と7行目です。

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import MemberList from './MemberList';   <----- ここ
import * as serviceWorker from './serviceWorker';

ReactDOM.render(<MemberList />, document.getElementById('root')); <---- ここ

serviceWorker.unregister();

これで修正は完了です。

出来上がり!

では、npm start してみましょう。

ビルドが完了するとテストサイト( http://localhost:3000 )でブラウザが自動で立ち上がります。
如何ですか?きちんとリストが表示され、ソート、フィルタ、チェックの操作ができましたか?

ポイントは、MemberList.jsでコンポーネントを追加し、index.jsで呼びだしをAppからMemberListに変えてやるところでしょう。 デフォルトで作られるApp.jsは不要になるので、削除しておkです。
(あと、見栄えのためにMemberList.cssも追加しとかないと綺麗に表示されませんのでご注意を)

謝辞

ひとまず React.js でスクローラブルリストをやってみました。
Vueとの違いがいくつかありましたが注意すべきは、HTMLを作成するコードを書くという観点と、イベントハンドラの呼び出しコンテキストと引数と思います。

次の記事で、この点を整理して要点を抑えておきたいと思います。
最後まで読んでいただきありがとうございます! 少しでもお役に立てたなら幸いです。
ではでは