【react】jqueryメインの非SPAシステムの特定ページでのみreactを導入してみたのでメモ

はじめに

以前に自分が作ったシステムで主にjqueryを使って作ったものがありました。
基本的には困ることはなかったのですが、特定のページだけ複雑になってきていじるのが嫌になってきました。。。

作りたい画面のイメージとしてはこんなかんじ

メイン画面
f:id:yoppy0066:20161030224945p:plain

モーダル1
f:id:yoppy0066:20161030225001p:plain

モーダル2
f:id:yoppy0066:20161030225010p:plain


・画面の中央にリストがある。
・「追加」ボタンで追加するデータの種別選択画面モーダルをひらく
・「次へ」ボタンで種別ごとに設定画面があるのでそれらへ遷移
・「登録」ボタンでモーダルを閉じてリストへ反映

リストへの登録を繰り返して、すべて登録できたら「サーバーへ送信」ボタンで登録が完了するようなイメージの画面。
最初はそこまで複雑でなかったのだが、種別が増えていって種別によっては2ページ、3ページかけて項目を入力するものはでてきた。。。

jQueryで作っていたのだが、さすがに1つのHTMLにすべてを書いてしまっていたので破綻気味になってきました。
で、けっこう悩んだけどreactjsを使うことにしました。

悩んだ理由としては
・この程度で大げさなきがした
・特定のページだけで使うというのを考えたことがなかったので使い方のイメージがパッとわかなかった

以前に試しでreact + reduxをつかったチャットアプリを作ってみたのだがさすがに今回はreactのみで。

実装

開発環境

今回はbrowserify、babelifyを使ってes6環境で開発してみた。
また、あとで出てくるけどreact-bootstrapっていうのをモーダルで使いたかったのでこれもいれた

package.json

{
  "name": "sample_react",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "watch": "watchify -v -t babelify src/index.js -o dist/index.js"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "watchify": "^3.7.0",
    "browserify": "^13.1.0",
    "babelify": "^7.3.0",
    "babel-preset-es2015": "^6.14.0",
    "babel-preset-react": "^6.5.0",
    "react-dom": "^15.1.0",
    "react-bootstrap": "~0.30.5"
  }
}

セットアップ

$ npm install
・・・
$ npm run watch
・・・

src以下にコードを追加していって、ビルドしたものがdist/index.jsに作られる。
なのでHTMLからはdist/index.jsを読み込んで使う

ディレクトリ構成
.
├─page1
│   ├── modal1.js
│   ├── modal2.js
│   ├── ・・・
│   └── type.js
└── index.js

SPAなシステムを作るわけではないのでreactを使うページごとにpage1、page2、page3・・・みたいな感じでディレクトリを増やしてゆくイメージ。

エントリーポイントを作る

index.js

import React from "react";
import ReactDOM from "react-dom";
import Page1 from "./page1/index";

// ★呼び出し側でpageを指定
var page = global.page;

if (page == "page1") {
	ReactDOM.render(
		<Page1/>,
		document.getElementById("my-app")
	);
}

よくないかもしれないが、この画面だけでもけっこうなボリュームだったので核となる部分だけreactで書き直すことにした。
なので、jQueryもつかってるので当然読み込む。

index.html

<link rel="stylesheet" href="/css/bootstrap.min.css">
<script src="/js/jquery.min.js"></script>
<script src="/js/bootstrap.min.js"></script>

<!-- ★ここはreactにいれられなかった(怠慢・・・) -->
<button id="btn-add">追加</button>

・・・

<div id="my-app"><!-- ★ここにreactで追加 --></div>

・・・

<script>
  var page = "page1";
</script>
<script src="./dist/index.js"></script>
メイン画面を作る

page1/index.js

import React from "react";
import Modal from 'react-bootstrap/lib/Modal';

import Type from "./type"; // 種別選択画面モーダル
import Modal1 from "./modal1"; // 種別1の入力画面モーダル
import Modal2 from "./modal2"; // 種別2の入力画面モーダル
・・・

export default class Page1Index extends React.Component
{
    constructor(props)
    {
        super(props);

        this.state = {
            mode: 1, // 1:種別選択画面 2:詳細設定画面
            type: 1, // 1:種別1 2:種別2 ・・・
            showModal: false,
            contentList: [
                { title: "タイトル1", description: "説明説明説明説明説明" },
            ]
        };

        this.setAppState = this.setAppState.bind(this);
        this.addContent = this.addContent.bind(this);

        <!-- ★react外で定義したボタンをここで操作・・・ -->
        $("#btn-add").on("click", () => this.setState({
            mode: 1,
            showModal: true
        }));
    }

    setAppState(params) {
        this.setState({
            mode: params.mode,
            type: params.type
        });
    }

    addContent(content) {
        this.state.contentList.push(content);
        this.setState({
            showModal: false,
            contentList: this.state.contentList
        });
    }

    render()
    {
        let modalClose = () => this.setState({
            showModal: false
        });

        let onDelete = (index) => {
            this.state.contentList.splice(index, 1);
            this.setState({
                contentList: this.state.contentList
            });
        };

        let onSend = () => {
            $.ajax({
                type: "POST",
                url: "http://www.example.com/",
                data: JSON.stringify(this.state.contentList),
                dataType: "JSON",
                scriptCharset: 'utf-8',
                success: function(result) {},
                error: function(err) {}
            });
        };

        var tableList = this.state.contentList.map(function(content, index) {
            return (
                <tr key={index}>
                    <td>{content.title}</td>
                    <td>{content.description}</td>
                    <td><button onClick={onDelete.bind(this, index)} className="btn btn-default">Delete</button></td>
                </tr>
            );
        });

        var modal;

        // 種別選択モーダル
        if (this.state.mode == 1) {
            modal = <Type show={this.state.showModal} onHide={modalClose} setAppState={this.setAppState} />;
        } else {
            // 種別別詳細入力モーダル
            if (this.state.type == 1)
                modal = <Modal1 show={this.state.showModal} onHide={modalClose} addContent={this.addContent} />;
            else if (this.state.type == 2)
                modal = <Modal2 show={this.state.showModal} onHide={modalClose} addContent={this.addContent} />;
            }
		
            return(
                <div>
                    <table className="table">
                        <thead>
                                <tr>
                                    <th>Title</th>
                                    <th>Description</th>
                                    <th>Delete</th>
                                </tr>
                        </thead>
                        <tbody>
                            {tableList}
                        </tbody>
                    </table>
                    <button onClick={onSend} className="btn btn-default">サーバーへ送信</button>
                    <Modal bsSize="large" aria-labelledby="contained-modal-title-lg"
                                  show={this.state.showModal}
                                  onHide={modalClose}>
                        {modal}
                    </Modal>
                </div>
            );
    }
}
種別選択画面モーダルを作る

type.js

import React from "react";
import Modal from 'react-bootstrap/lib/Modal';

export default class Page1Type extends React.Component
{
    constructor() {
        super();
        this.state = {
            type: null
        };
    }
	
    change(e) {
        this.setState({
            mode: 2,
            type: e.target.value
        });
    }
	
    render()
    {
        let onNext = () => {
            this.props.setAppState({
                type: this.state.type
            });
        };

        return(
            <div>
              <Modal.Header closeButton>
                  <Modal.Title id="contained-modal-title-sm">種別を選択して下さい</Modal.Title>
              </Modal.Header>
              <Modal.Body>
                    <div className="radio">
                        <label>
                            <input type="radio" name="radio" value="1" onChange={this.change.bind(this)} /> 種別1
                        </label>
                    </div>
                    <div className="radio">
                        <label>
                            <input type="radio" name="radio" value="2" onChange={this.change.bind(this)} /> 種別2
                        </label>
                    </div>
            ・・・
            </Modal.Body>
            <Modal.Footer>
                <button onClick={onNext} className="btn btn-primary">次へ</button>
                <button onClick={this.props.onHide} className="btn btn-default">Close</button>
            </Modal.Footer>
        </div>
        );
    }
}
種別1の入力画面モーダルを作る

modal1.js

import React from "react";
import Modal from 'react-bootstrap/lib/Modal';

export default class Page1Modal1 extends React.Component
{
    constructor() {
        super();
        this.state = {
            title: "",
            description: ""
        };
    }
	
    render()
    {
        let onSave = () => {
            this.props.addContent({
                title: this.refs.title.value,
                description:this.refs.description.value
            });
        };
		
        return(
            <div>
                <Modal.Header closeButton>
                    <Modal.Title id="contained-modal-title-sm">種別1の設定をして下さい</Modal.Title>
                </Modal.Header>
                <Modal.Body>
                    <div className="form-group">
                        <label>Title</label>
                        <input type="text" className="form-control" ref="title" placeholder="Title" />
                    </div>
                    <div className="form-group">
                        <label>Description</label>
                        <input type="text" className="form-control" ref="description" placeholder="Description" />
                    </div>
                </Modal.Body>
                <Modal.Footer>
                    <button onClick={onSave} className="btn btn-primary">Save</button>
                    <button onClick={this.props.onHide} className="btn btn-default">Close</button>
                </Modal.Footer>
            </div>
            );
    }
}

おわりに

今回はちょろっとしたサンプル書きましたが実際はモーダルの数がけっこう多くなりました。
ずらずら書きましたが、こんな感じでreact-routerとかも使わないレベルのものでもjQueryで全部実装するよりは見通しがよくなったのかなと思いました。

仕事でがっつりSPAを開発となるとハードル高そうだけどちょっとずつ導入していくのもありなのかなと思いました。以上です