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

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

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

はじめに

前回、Reactをやってみるってことで3ステップで環境構築してみました。
今回はこれをベースにして、以前、Vueで作った『スクローラブルリスト』を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形式で定義しています。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>
  );   
}

dhead() メソッドでは、JSX (JavaScript XML) というマークアップ言語を記述するためのXMLシンタックスに従ってリストのヘッダ部分のHTMLを生成するロジックを記述しています。

そしてヘッダ部の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)}>

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

handleAdminChanged = (member) => {
  let list = this.state.members;
  const target = list.find(rec => {
    return rec.id === member.id
  })
  target.admin = !target.admin;
  this.setState({members: list})
}

引数の定義などは `sortBy()'と同じなので割愛しまぁ~す (謝)

dlist = () => {
  return (
    <div className="data-field">
      {
        this.state.members.map((member, row) => {
          return (
           <div className="table-row wrapper attributes data" key={row}>{
              this.columns.map((name,idx) => {
                if(name === "admin") {
                  return (
                    <div className={name} key={idx}>
                      <Form.Check
                        type="checkbox" variant="success" checked={member[name]}
                        onChange={() => this.handleAdminChanged(member)}
                      />
                    </div>
                  )
                } else if(name === "progress") {
                  return (
                    <div className={name} key={idx}>
                      <ProgressBar variant="success" now={member[name]} label={`${member[name]}%`}/>
                    </div>
                  )
                } else {
                  return (
                    <div className={name} key={idx}>
                      {member[name]}
                    </div>
                  )
                }
              })
            }</div>
          );
        })
      }
    </div>
  );
}

dhead() メソッドと同じく JSX でリスト部分のHTMLを生成するためのロジックを記述しています。
5~35行目の this.state.members.map((member, row) => {...} の中でメンバオブジェクトの配列の要素ごとにHTMLを生成しています。

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を書くのではなく、HTMLを生成するロジックをコードで書くという観点と、イベントハンドラの呼び出しコンテキスト、引数と思います。

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