Help us understand the problem. What is going on with this article?

jQuery愛好家のためのVue.js、React入門(いずれAngularも)

まず、ことわっておきますが、jQueryは非常に優秀なライブラリです。自分がメインとするWEBシステムの世界ではかなり重宝していますので、時代の潮流だからといって理由もなくjQueryを切り捨てろとは一言も言いませんし、もっと技術は培養すべきです。

ですが、使用できる選択肢を増やす、武器を増やすためにVue.js、Reactを学習するのは非常に有効です。ただ、これらの活用は学習コストが高いといわれています。その原因はフロントエンドありきで話が進みすぎているからだと考えています。したがって、自分の投稿記事は、jQueryを多用するWEBシステムエンジニアに向けた、フォーム操作をメインに置いた半備忘録兼自分なりに解釈した解説です。

ちなみに自分はサーバ構築からバックエンドまでこなしているワンオペエンジニア(フリーランス、非正規雇用に非ず)です。

§1:Vue.jsとReact、そしてAngular

その前に、vue.jsとReactとはどういったもので、どんな意図で開発されたものかを知っておく必要があります。そして、その理念を知っていたら、スクリプト記法の理屈もわかりやすいからです。

ひとまず、共通のスローガンがあります。それはいずれも素早くデータの同期をとりたいために実現させている技術であることです。

Vue.js

Vue.jsはもともとGoogleが開発したAngularJSの開発者の一人が、個人で開発を始めたJSフレームワークです。そのため、小規模の開発に向いた柔軟な利用も可能です(デフォルトでjQueryとの混用も可能)。そのVue.jsとは一言でどんな技術かというと

html内の部品に対して、Vueディレクティブという魔法をかける

こういうものです。ディレクティブとはVue.jsで定義されるv-xxxxというコマンドのことで、データのインプットとアウトプットをリアルタイムで行うことができたりする同期(バインド)操作のことです(訂正指摘感謝します)。このVue.jsでのバインドを、Reactではリアクティブ、AngularJSなどでは双方向バインディングなどと呼んでおり、今のAngularでもその技術はある程度継承されています。そして、AngularJSはhtml側でngというプロパティやメソッドを使ってバックエンドの処理をしており、かなり明解だった反面、開発が進むとかなりhtmlソースが汚されてしまうので、技術をある程度継承し、Angularというパッケージ単位のフレームワークを作ったわけです。しかし、このAngularJSの双方向バインディングと簡潔さを捨てるのはもったいないと、飛躍的に発展させたのがVue.jsでありVueディレクティブというもので、これはソースが汚れる原因となったhtml側でのバックエンド処理を、スクリプト側(コンポーネントを作成して管理)で処理させるようにしました。つまり

  • AngularJS: 双方向バインディング処理をhtml側で処理できたために、ソースが汚れてしまい、敬遠されてしまった。
  • Vue.js  : バインド(双方向バインディングとほぼ同定義)処理をスクリプト側に記載させるようにし、html側は基本、メソッドとプロパティだけ入出力させるようにした(つまり、JavaSrciptの基本に返った)

この理念を覚えておけば、今後の学習にも役立ちます。

React

ReactはFacebookが開発したコンポーネント型のJSフレームワークで、その鍵となるのはJSXという記法です。そのJSXとはなにかというと、スクリプトの中に直接htmlタグを記述できてしまうもので、それを作った理由は後述するように完全なるデザイン部分とプログラム部分の切り分け(すなわち部品管理の徹底化)のためです。ですが、その独特の記法によって技術者や入門者に違和感を与え敬遠されてしまっていたのも事実で、そのために何度も記法が変更されてきており、今日ではだいぶ見やすく、そして記述しやすくなっています。それでもReactの基本的な理念と動きは変わっておらず、一言でいうと

html内に魔法結社(JSXの拠点)を作り、そこでリアクティブな部品(htmlタグ)を錬金する

こういうものです。つまり、Vue.jsだといわゆるリアクティブな部品(Vueディレクティブ)自体はhtml上にあったのに対し、Reactの場合は、リアクティブな部品はhtmlになく、バックエンド上のコンポーネントで作成されることになります。そして、リアクティブな処理を行う際もそのバックエンド内だけで処理するので、非常に動きが高速で、部品もバックエンドにしか存在しないことで、開発を分担しやすく中規模の開発に向いています。

  • React :部品の管理を徹底するために、リアクティブな処理をhtml側で一切できないようにしている。部品の調達と管理はすべてJSXに則ったコンポーネント内で行う。

これが基本です。あと、コンポーネントの記法もいろいろあって、しかもしょっちゅう変更されているので、それが却って敬遠させている(初心者がどこから手を付けたらいいのかわからない)気もしますが、基本となる部分は全くぶれていないことを踏まえておいてください。

Angular(参考)

AngularはAngularJSでの失敗を教訓に、その失敗の原因となったソースの汚れを解消させたものです。そしてvue.js、Reactと違い、RubyのRailsやPHPのLaravel、PythonのDjangoのような、それ一つでパッケージとなっているフルスタックフレームワークとなっており、TypeScriptがベースです。そして、これも一言で表すと

パッケージ自体がAngularという魔法世界(フレームワーク)である

なので、そのパッケージ内では自由自在にAngularの技術を利用できます(VueやReactのように、どこからどこまでという範囲指定が不要)。ですが、前述したようにソースの汚れを反省して作ったフレームワークなので、いわゆるリアクティブな部品は各アーキテクチャ内で実行するようになっています。ただ、基本はAngularJS時代とそこまで変わっていないためにあまり難しくなく、どちらかというとVue.jsに近いですが、Vue.jsより理屈は解りやすいので、フレームワーク開発に慣れているバックエンドエンジニアなら、上記2つより学習は楽かもしれないです。ただ、けっこう容量があるので、大規模開発向きです(これを省力化、小規模化したJSフレームワークも存在します)。

  • Augular:バインディング処理を行う部品はhtml上にあるが、処理は外部のアーキテクチャ内で行う。

なお、現在もAngularJSはマイナーチェンジを続けていますが、本来のAngularJSの目的はVue.jsが担っていると考えていますので、本記事でAngularJSは採り上げません。

§2:バックエンドのための基本文法

前述したように、これらのJSフレームワークの学習コストが肥大してしまった原因は、フロントエンドありきで解説していることが多いためでしょう。そのため、バックエンドエンジニアが基礎の基礎である文法もわからずに、そっちばかりに目が行ってしまっていることで混乱を招き、これらの技術に手を出す気力を与えず、比較的記述が簡単なjQuery依存から脱皮できなかったのではないかと考えています(自分もそうでした)。

したがって、自分はjQueryで操作してきたことを、Vue.js、Reactではどう記述するのかを重点において解説していきたいと思います。

演習1:フォーム操作:テキスト文字を表示させる

フォーム操作の基本の基本です。ですが、この基本だけでかなり記法の根本が理解できるのも事実です。ただ、単純に表示させるだけなのも面白くないので、jQueryでフォームに対し、キーの打鍵ごとに値を表示させるようにしましょう。

jquery-sample1.html
<body>
<input type="text" id="f_inp">
<p>入力された文字<span id="mes"></span></p>
<script>
$(function(){
    $('#f_inp').on("keyup",function(){
        let mes = $(this).val(); //値を取得する
        $("#mes").text(mes); //打ち込んだ値を反映させる
    })
})
</script>
</body>

これで、打ち込んだ文字をそのまま下の#mesに表示させることができます。これとほぼ同じ挙動をVue.js、Reactで再現してみます。

Vue.jsで再現

Vue.jsで記述するとこうなります。そしてVue.jsですが、こちらはコンテンツの後に制御部分を記述してください。

vue-lesson1.html
<script src="https://cdn.jsdelivr.net/npm/vue"></script>
</head>
<body>
    <div id="app">
        <input type="text" v-model="mes">
        <p>入力された文字<span>{{ mes }}</span></p>
    </div>
<!-- 制御部分はコンテンツの後 -->
<script>
 new Vue({
        el: '#app',
        data:{
            mes: '',
        },
 });
</script>
</body>

jQueryより更に簡潔になっているのは一旦置いといて、目の付け所はelというプロパティとv-modelというVueディレクティブであり、これこそがまさしくVue.jsの要であるバインド技術になります。そして、それが適用される部品は任意のIDで囲んだ単一の親要素のみとなります。これをエレメントと呼び、el(elementの略)プロパティで指定した範囲のみ、ディレクティブを適用できます。

もっと詳しく文法を解説する

Vue.jsの場合は、html部分とスクリプト部分は区別しない方が説明もしやすいです。もう一度さっきのhtmlファイルを確認してみましょう。そして、コメントを付与します。

vue-lesson1.html
<body>
    <div id="app"><!--#app内のブロック要素にVueディレクティブを適用(1) -->
        <input type="text" v-model="mes"><!-- v:modelはフォームの値を監視するVueディレクティブ(2) -->
        <p>入力された文字<span>{{ mes }}</span></p><!-- {{hoge}}はマスタッシュ -->
    </div>
<script>
 new Vue({
        el: '#app', //適用対象となる要素名(1)
        //dataはVue.jsの操作に必要な変数を格納するオブジェクト
        data:{
            mes: '', //今回は変数mesを使うので定義しておく(中は空白)。(2)
        },
 });
</script>
</body>

このようになります。まずは、部品をバインドするために(1)のように、どこまで適用するのか、その場所を定義し、そこから具体的な動作(2)が行われます。v-modelはフォーム部品の動きを監視する働きを持っているので、inputの動きに変化(新たに文字が入力された)があると即座に反応し、そしてその結果を{{ mes }}に返すようになっています。

補足1:dataプロパティ

dataプロパティはVueディレクティブで作業するための変数置き場で、これを定義しておかないと、xxxx is not definedと処理中に未定義のエラーが起きます。今回はmesという変数を使用しているので、これを定義しておき、そして初期値を空っぽにしておきます。

補足2:マスタッシュ

マスタッシュとは英語で口髭のことで、{{ }}という記号です。そしてVue.jsではVueディレクティブの外側(v-modelなどv-xxxxというプロパティ)に値を適用する場合は{{変数名}}とすることで、その値を受けとることができます。ここでは{{mes}}はVueディレクティブの外側なのでマスタッシュで記述しています(エレメントの外側ではないので注意。エレメントの外側にマスタッシュを記述しても、そこはVue.jsの適用外なので、ただの文字列として認識されるだけです)。

Reactで再現

では、全く同じ動作をReactでも再現してみます。ただ、Reactは少し説明をわかりやすくするために、敢えて最短の記述にしていません(もっとスリム化した記法も使えるのですが、今日、一番見ることが多い記法です)。

react-lesson1.html
<script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/6.10.3/babel.min.js"></script>
<script type="text/babel">
class App extends React.Component{
    constructor(){
        super();
        this.state = {
            mes: ''
        };
        this.changeText = this.changeText.bind(this);
    }
    changeText(e){
        this.setState({ mes :e.target.value });
    }
    render(){
        return(
            <div>
                <input type="text" onChange={this.changeText} />
                <p>入力された文字<span>{ this.state.mes }</span></p>
            </div>
        );
    }
}
ReactDOM.render(
    <App />,
    document.getElementById("root")
);
</script>

</head>
<body>
    <div id="root"></div>
</body>

初見の人が見たら、「なんだこりゃ?」となること請け合いです。スクリプトの中にhtmlタグが入っているのが生理的に受け付けないのでしょう。しかし、Reactも初期と比較すると、だいぶ記述はスリム化しており、そして解りやすくなっています。では、この処理の流れがわかるようにコメントを付与してみます。なお、Vue.jsでは、スクリプトをbodyタグの内側、コンテンツの後に書くのがセオリーでしたが、Reactはコンテンツの前、headタグの中にスクリプトを記述するのがセオリーです(普通は外部ファイル化します)。

react-lesson1.html
<script type="text/babel">
//Reactコンポネントを作成するクラス(1)
class App extends React.Component{
    //A:定義部分。簡単にいえば、処理前に準備する変数やメソッドなど。
    constructor(){
        super();
        this.state = {
            mes: ''
        };
        this.changeText = this.changeText.bind(this); //(3)値をバインドさせる準備
    }
    //B:処理関数部分。renderの外側に書く方が見やすい。
    //入力された文字を返す関数(2)
    changeText(e){
        this.setState({ mes :e.target.value }); //setStateは値をバインドさせる処理(3)
    }

    //C:レンダリング部分。JSX記法のhtmlタグを描画する。
    render(){
        //JSX記法で返す部品(2)
        return(
           //onChangeハンドラ以下はReactでバインディング処理を行う関数(3)
            <div>
                <input type="text" onChange={this.changeText} />
                <p>入力された文字<span>{ this.state.mes }</span></p>
            </div>
        );
    }
}
//コンポーネントの反映
ReactDOM.render(
    <App />, //コンポネント作成を行う部品(1)。記述方法にルールがある。
    document.getElementById("root") //外側に部品を表示させる(4)
);
</script>

</head>
<body>
    <div id="root"><!-- ここに処理部品が入る(4)--></div>
</body>

だいたいこのような流れになっています。16.8からはもっと簡略化した記法も使えるのですが、ひとまず一般的に知られている記法を試してみてからの方がいいでしょう。見た目は複雑に見えますが、まずは次のように分類しましょう。

  • React.Component() //コンポネントを作成します
  • ReactDOM.render() //作成したコンポネントを反映させます

renderとはいろいろな意味がありますが、レンダリングという言葉通り、描写という言葉で覚えればいいでしょう。そして、その名の通り、ReactDOM.render内ではJSX記法に則ったhtmlタグを描写しているのです。そしてその部品を作成するのがReactDOM.Componentであり、いわば、前者がシステムの納入、後者がシステムの開発や保守管理だと思えばいいでしょう。したがって、htmlタグ側にはリアクティブ処理された部品しか存在していなく、スクリプトの処理部分は、Vue.jsと違って部品すら見えません。

では、今度はReact.Componentの中身を3つに分類してみます。

  • constructer() //定義部分
  • 処理用の関数 //処理部分
  • render() //レンダリング部分

だいたいはこのように分類するとわかりやすいです。そして、その作業の流れを文章化してみると

  1. Appという部品を作成する命令を出す(1)
  2. 命令を受けたコンポネントはコンストラクタから部品を用意してレンダリング領域にある通り、タグを作成し、ReactDOM.renderを使用して処理の外側に返す(2)(4)
  3. onChangeイベントが発火したら、用意していた関数を使って処理を行う(3)
  4. 処理を行った状態で再度レンダリングを行い、ReactDOM.renderを使用して処理の外側に返す(4)

それを踏まえた上で処理を追ってみるとやっていることはJavaScriptとそこまで相違ないと気づくはずです。

ただ、決定的な違いとして処理関数内にreturnが存在しない(レンダリング部分ではない)、そしてreturn処理を行う代わりにsetStateで制御することです。そして、これによってバインド処理が可能になります。

オブジェクト、変数の利用

Reactでもう一つ躓きがちなのが、オブジェクトや変数の利用に際しては、同じコンポーネント内であっても、クラスオブジェクトで制御しているために外部の存在として扱わないといけないということです(オブジェクト指向におけるthisのやりとりを連想してください、それとほぼ同じ理屈です)。具体的に言うと、thisという代名詞を置いているのがそれで、それぞれ、定義部分、レンダリング部分、関数部分で変数や関数をやりとりする際には、thisという代名詞を使っているのがわかると思います。

  • 関数部分のchangeTextをレンダリング部分で使用する場合、this.changeTextとして呼び出し
  • レンダリング部分でmesを使用するために、定義部分のstate.mesをthis.state.mesとして呼び出し

このような具合です。またsetStateのようにコンポーネントで用意された部品を使用する際も、外部から借用することになるので、this.setStateと代名詞を付与しています。

補足1:バインディングのルール

定義部分で

    this.changeText = this.changeText.bind(this);

とあると思いますが、これは別におまじないではなく、利用する関数において値をバインド(同期)したいときに記述するルールで、これを記述しないと値をバインドできません。そして、基本は同じオブジェクト名にしておくだけで、左側は変数を代入しているだけですので、定義さえしておけば、別名でも大丈夫です(したところでメリットが薄いので同名にしおくべきですが)。

また、setStateはReactにおいて極めて重要性の高いメソッドですが、簡単にいえば、元の値と新しい値をバインドさせたいときに設定するものです。具体的には先程入力した文字と新たに入力した文字が異なる場合、随時setStateメソッドが実行されることになります。

また、e.target.valueはJSX内にある部品の、任意のフォームに対して取得した値(value)を受けとるもので、e(jQueryではelemと書くことが多い)とは、任意のオブジェクト変数に過ぎません。そして、受け取った値を先程のsetStateメソッドを使って、値の変化を処理しているわけです。

補足2:JSXの記述ルール

JSXは共通して以下の決まりがあります。

  • 単一の親要素しか生成してはいけない(中に子要素、孫要素があるのは問題ない)。もし、複数のタグを使いたい場合は、次の例のようにFlagmentオブジェクトを使ったり、タグごとにオブジェクト化する方法があります。
  • <input>タグのようにそれ一つで完結するタグは必ず<input />のようにスラッシュを入れること。これを入れないとJSXがどこまで適用したらいいのかわからずにエラーが起きます。これはテンプレートタグ<App / >などでも同様です。
  • JSXで生成したhtmlは必ず丸括弧(...)で囲って値を返すこと。これは次章のmapメソッドでも活用します。

最新の書き方(16.8以降)

上記の方法でも新たに導入されたクラスオブジェクトを使用することでかなり簡潔になりました(ECMA5時代はコンポネントも逐一作成する必要がありました)が、それでも至るところにthisばかりあったりと、色々と無駄があるようにも見えました。それをスリム化させるとこのようになります。

react-renew.html
<script type="text/babel">
    //定義部分
    const { useState } = React; //ローカルの場合で、useStateを使用するための定義
    const App =()=>{
        //処理部分
        const [mes,setMes] = useState(true); //バインディング処理
        //一度メソッドで準備する
        const changeText = (e)=>{
            setMes( e.target.value ); //値の比較
        }
        //レンダー部分
        return(
        <div>
            <input onChange={changeText} />
            <p>入力された文字<span>{ mes }</span></p>
        </div>
            );
    }
        const elem = <App />; //ワンクッション置かないと警告が出る
    //レンダリング処理
    ReactDOM.render(
        elem,
        document.getElementById("root")
    );
</script>

代名詞のthisが消えてかなり明白になったと思いますが、基本的な動作は変わっていません。ただ、変数定義部分、処理部分、レンダリング部分が一つの関数に収まったので、すごくすっきりしています。

ただ、注意することとして

html
    const App =()=>{....}
    const.elem = <App />; //ワンクッション置く
    ReactDOM.render(
        elem,
        document.getElementById("root")
    );
    //ダイレクトに呼び出す以下の書き方は推奨されていない(警告が出ます)
    const App =()=>{....}
    ReactDOM.render(
        <App />,
        document.getElementById("root")
    );

この部分で、このようにrenderメソッドに対して、ワンクッション置かないと警告メッセージが表示されることになります。

演習2:プルダウンメニューの制御

では、今度は繰り返し処理の実例としてプルダウンメニューの生成とイベント処理をそれぞれ記述していきます。そして、共通の動作としてプルダウンメニューをオブジェクトから生成し、選択した値を表示させるという動きをそれぞれjQuery、Vue.js、Reactで再現してみます。

そうすれば、共通点と押さえどころが見えてくるはずです。

jQuery

まずはjQueryで記述してみます。ループ式の記述方法は色々ありますが、今回は敢えて$.eachメソッドを使います。なお、jQueryはあくまでJavaScriptライブラリなので、ECMA6でも普通に記述できます(jQueryでは敢えて見慣れた記法にしています)。

jquery-sample2.html
<script>
    $(function(){
        //配列の値
        let ary_data = [
            {key:0,name: "cakePHP"},
            {key:1,name: "Laravel"},
            {key:2,name: "CodeIgniter"},
            {key:3,name: "Symfony"},
            {key:4,name: "Zend Framework"},
            {key:5,name: "Yii"},
        ];
        let sel = $("#sel");
        //プルダウンメニューの生成
        $.each(ary_data,function(key,data){
            opt = $('<option>').val(item.key).text(item.name);
            sel.append(opt);
        })
        sel.appendTo("#f_sel");

        //プルダウンの操作
        $("#sel").on("change",function(){
            let selkey = $(this).val();
            $("#txt").text(ary_data[selkey].name);
        })
    })
</script>
</head>
<body>
    <div id="f_sel">
        <select id="sel">
            <option value="">選択</option>
        </select>
    </div>
    <p>選択された値:<span id="txt"></span></p>
</body>

このシステムのポイントは前述した通りで、

html
$.each(ary_data,function(key,item){
    opt = $('<option>').val(item.key).text(item.name);
})

このループ式の部分です。これはary_dataという配列をitemという変数に格納しながら順番に展開させ、item.keyをvalueプロパティに、item.nameをoptionタグのテキスト値に代入している工程になりますが、実はこれと全く同じ工程が次のVue.jsとReactにも現れます。

Vue.js

同じ動作をVue.jsで記述するとこうなります。

vue-lesson2.html
<body>
    <div id="app">
        <select v-model="sel">
            <option value="">選択</option>
            <option v-for="data in ary_data" :value="data.name" >{{ data.name }}</option>
        </select>
        <p>選択された値:<span id="txt" >{{ sel }}</span></p>
    </div>
<script>
new Vue({
    el: "#app",
    data:{
        sel: "",
        ary_data :[
            {name: "cakePHP"},
            {name: "Laravel"},
            {name: "CodeIgniter"},
            {name: "Symfony"},
            {name: "Zend Framework"},
            {name: "Yii"},
        ]
    }
})
</script>

非常にすっきりした内容になりました。後述するReactと比較してもVue.jsはフォーム操作に向いており、何よりv-modelプロパティを使用するだけで値を一発で同期できるのが魅力的ですね。

さて、先程申し上げたように、この記述での目の付け所はv-for="data in ary_data"という部分であり、これは先程のjQueryの$.eachと同様に配列分のoptionタグに対してvalueプロパティとテキスト値を付与するループ処理となっています。forはPHPのfor文などでもお馴染みですが、よく知られる「~のために」という意味のほかにも「~している間」という意味があり、その名のとおり配列ary_dataを展開している間はdataという変数に格納しているわけです。しかもjQueryでは逐一スクリプト文で記述していましたが、Vue.jsはこのVueディレクティブの魔法の力(?)でスクリプトに数式を記述することなくループ処理が行えるので、かなり記述が楽になります。なお、ループできるのは何もoptionタグだけでなく、リスト作成に用いるliタグやテーブル作成のtdタグ、はたまたテンプレートタグを用いれば複数のタグをひとまとまりでループさせることも可能です。

補足:Vueディレクティブの省略記法

  • v-bind:xxxx → :xxxx
    上記のoptionにあるvalueと:valueは別物で、:valueはVueディレクティブにおけるv-bind:valueの省略記法です。v-bindは非常に使用頻度が高いので、このように省略記法を使うことが多く、慣れてきたら使ってみるといいでしょう。なお、v-bindとはhtmlタグ内のプロパティに、任意の値を同期させるプロパティで、この場合v-forを展開している間に、valueプロパティにその値を格納させる働きを持っています(あくまでhtmlの内側です。外側でvueディレクティブは使えないので、そちらでは前述したように{{}}(マスタッシュ)を使用します。

  • v-on:xxxx → @xxxx
    もう一つ使用頻度が高い省略記法が上記のもので、これはイベントメソッドを実行するときに使用し、同じく使用頻度が高いv-onを省略させるものです。これは次章で採り上げます。

React

同じようにプルダウンメニューの生成をReactでも記述してみます。

react-lesson2.html
<script type="text/babel">
const {useState } = React;
const App =()=>{
    //変数定義
    const [sel,setMenu] = useState(true);
    const ary_data = [
        {key: 1,name: "cakePHP"},
        {key: 2,name: "Laravel"},
        {key: 3,name: "Code Igniter"},
        {key: 4,name: "Symfony"},
        {key: 5,name: "Zend Framework"},
        {key: 6,name: "Yii"},
    ];
    //値の設定処理(いわばVue.jsの算出プロパティに当たる部分)
    const setList = ary_data.map((data)=>(
        <option key={data.key} value={data.name}>{data.name}</option>
    ))
   //イベント処理
    const changeMenu = (e)=>{
        setMenu(e.target.value ); //フォームから取得した値
    }
    //レンダリング
    return(
        <React.Fragment>
        <select onChange={changeMenu}>
            <option>選択</option>
            { setList }
        </select>
        <p>選択された値:<span>{sel}</span></p>
        </React.Fragment>
    );
}
const elem = <App />;
ReactDOM.render(
    elem,
    document.getElementById("root")
);
</script>
</head>
<body>
    <div id="root"></div>
</body>

Vue.jsと比較すると色々とややこしくは見えますが、それでも前述したReactとそこまで動作は変わっていませんし、やはりこれも同様にループ式によって配列を展開しています。その箇所がmapメソッドで、このmapメソッドはどちらかというとjQueryの$.eachメソッドに近く、コールバック関数を使って値を展開する働きを持っています。後は、他の2例と同様にvalueプロパティやテキスト値にkeyやnameを配列順に代入しているだけです。

//設定処理 ary_dataを順々に展開し、data変数に格納。該当するプロパティをそれぞれ当てはめていく。
    const setList = ary_data.map((data)=>(
        <option key={data.key} value={data.name}>{data.name}</option>
    ))

オブジェクトリテラルについて

さて、先程までは触れていませんでしたが、Reactの記法を見ていているとJavaScriptやVue.jsで見られたクォート("")はどこにも書かれておらず、代わりに{ ... }という波括弧があちこちに存在し、その役割が気になったと思います。これはオブジェクトリテラルというもので、JSX内において外側に置いている変数(オブジェクト、メソッドなども含む)を展開するために用いるものです。それを踏まえて動きを追ってみるとJSXのselectタグの内部にsetListという変数を展開し、子要素のoptionタグを代入していることがわかります。また、changeイベントの実行時のメソッド、changeMenuはプルダウンメニューの値が変更した際に行うイベント処理を展開しており、それによって先程と同様、直前と現在の値を比較し、値を出力させるようになっています。

補足

Reactでは、JSXにおけるレンダリングのルールとして、単一の親要素しか作れないというものがあります。ですが、今回のようにプルダウンメニューとPタグなど複数のタグを生成したい場合は、上述のように<React.Fragment>というテンプレートで括る方法や、オブジェクトに格納して返す方法などがあるようです。

演習3:検索フォーム

さて、今までの演習ではjQueryとその他の技術でそこまで工数に差が発生していませんでした。それはイベントがほとんど起きておらず、処理もあまり行われていないからです。しかし、次の演習に用意した検索システムだとどうでしょうか。

では、テキストボックスに入力した文字をもとに、リスト一覧から該当する要素を出力するという機能をそれぞれjQuery、Vue.js、そしてReactで実装してみます。なお、この一見単純な動きですが、これがよくある写真共有サイトの基礎となる動きです。

jQuery

まずは、あえてjQueryで記述してみます。構築の仕方は色々ありますが、結論から言って面倒です。

jquery-sample3.html
<script>
    $(function(){
        //配列の値
        ary_data =[
            {key: 1,name: "モスクワ",},
            {key: 2,name: "サンクトペテルブルク"},
            {key: 3,name: "エカテリンブルク"},
            {key: 4,name: "ムンバイ"},
            {key: 5,name: "ベンガルール"},
            {key: 6,name: "コルカタ"},
            {key: 7,name: "サンパウロ"},
            {key: 8,name: "リオデジャネイロ"},
            {key: 9,name: "ブラジリア"},
        ];
        let ul = $("#list");
        let li;
        let opt;
        //プルダウンメニューの生成
        $.each(ary_data,function(key,item){
            opt = $('<li>').text(item.name);
            ul.append(opt);
        })

        //プルダウンの操作
        $("#word").on("change",function(){
            let word = $(this).val();
            li = ul.find('li');
            li.remove(); //リストの初期化
            $.each(ary_data,function(key,item){
                    //部分一致検索
                if(item.name.indexOf(word) !== -1 ){
                    opt2 = $('<li>').text(item.name);
                    ul.append(opt2);
                }
            })
            $("#len").text(li.length);
        })
    })
</script>
</head>
<body>
    <div id="app">
        <input type="text" id="word">
        <p>検索結果<span id="len"></span></p>
        <ul id="list"></ul>
    </div>
</body>

一度リストを形成しているのに、イベントが起きるごとに再度DOMを再構築しているので、動作に無駄があるように思われます。このように検索機能など、何度もレンダリングが必要になる動作はjQueryはあまり向いていません(detachで隔離して不要なデータだけ除去するという技も別の記事で紹介してますが…)。

Vue.js

同じ動作をするシステムをVue.jsで書くとこのようになります。そしてここから算出プロパティ、メソッド、そして監視プロパティにも触れていきます。

vue-lesson3.html
<body>
    <div id="app">
        <input type="text" v-model="word">
        <p>検索結果{{filterData.length}}件</p>
        <ul>
            <li v-for="data in filterData">{{ data.name }}</li>
        </ul>
    </div>
<script>
new Vue({
    el: "#app",
    data:{
        word: "",
        ary_data :[
            {key: 1,name: "モスクワ",},
            {key: 2,name: "サンクトペテルブルク"},
            {key: 3,name: "エカテリンブルク"},
            {key: 4,name: "ムンバイ"},
            {key: 5,name: "ベンガルール"},
            {key: 6,name: "コルカタ"},
            {key: 7,name: "サンパウロ"},
            {key: 8,name: "リオデジャネイロ"},
            {key: 9,name: "ブラジリア"},
        ]
    },
    //算出プロパティ
    computed:{
        filterData: function(){
            let word = this.word
            let ary_data = this.ary_data
            let lists = []
            if(word != ""){
                ary_data.filter(function(elem,idx){
                    if(elem.name.indexOf(word) !== -1){
                        lists.splice(idx,1,elem)
                    }
                })
                return lists;
            }
            return ary_data
        }
    }   
})
</script>

■算出プロパティ(computed)

今まではあまりjQueryとVue.jsでそこまで動きや記述に差はありませんでしたが、動的なイベントが増えてくるとその本領が発揮されてきます。特に画期的な機能がcomputedプロパティによって機能する算出プロパティでしょう。これはv-modelなどでリアクティブに依存している値が変更された場合のみ、即座にその情報をキャッチして処理を行うものです。裏返せば、値が不変の場合は何も反応しないという優れた性能を持っています。

具体的には検索用のテキストボックスはv-modelディレクティブで値をバインドしているため、逐次検索文字が変わるごとにfilterDataメソッドが実行され、その結果を返します。当該メソッド内ではindexOf()メソッドによって値のマッチングを調べているので-1(検索結果なし)以外は反応するようにし、それに従いspliceメソッドによって検索結果を新たなlistオブジェクトに代入するようにしています。そしてその結果をv-forディレクティブの初期値に設定し、値をループさせているのです。そして、その説明でお気づきだと思いますが、算出プロパティに引数は不要どころか無用です。また、この算出プロパティは値がキャッシュされるので、それによって高速な値のバインド処理が可能になっています(それによる弊害もあるのですが、それは次の説明で…)。

なお、イベント自体を監視するwatch(監視プロパティ)やcreated(ライフサイクルフック)などもありますが、まずはcomputedと次に挙げるmethodsだけに話を絞ります。

■メソッド(methods)

対してメソッドというのものがあり、methodsプロパティ以下に記述します。それが一般にJavaScriptで使われてきたonXxxxxというプロパティで制御するメソッドのように思いがちです。ちなみに、これをVueディレクティブではv-on:xxxxと記述し、前章で説明した通り@xxxxと省略することができます。ところが、Vue.jsではフォーム部分の制御は概ねv-modelでバインドしてくれるので、そこに記述する必要はありません。では、どのようなときに使用するかといえば

  • (1)リアルタイムな値のバインドが不要なイベント制御
  • (2)画像ファイルなど値をキャッシュされては困るものの制御
  • (3)算出プロパティなど関数内での、内部で再計算などの処理が必要な場合

これらのケースで使用することが多いです。具体的な例を挙げましょう。

vue-lesson3b.html
<body>
    <div id="app">
        <input type="text" v-model="word">
        <p>検索結果{{filterData.length}}件</p>
        <button @click="clear">文字のクリア</button><!-- clearはmethodsのケース1-->
        <ul>
            <li v-for="data in filterData">
            <dl>
            <dt>{{ data.name }}</dt>
            <dd ><img :src="imagePath(data.img)"></dd>
                        </dl>
            </li>
        </ul>
    </div>
<script>
new Vue({
    el: "#app",
    data:{
        word: "",
        ary_data :[
            {key: 1,name: "モスクワ",img:"Moscow.jpg"},
            {key: 2,name: "サンクトペテルブルク",img:"Sankt.jpg"},
            {key: 3,name: "エカテリンブルク",img:"Yekaterin.jpg"},
            {key: 4,name: "ムンバイ",img:"Mumbai.jpg"},
            {key: 5,name: "ベンガルール",img:"Bengaluru.jpg"},
            {key: 6,name: "コルカタ",img:"Kolkata.jpg"},
            {key: 7,name: "サンパウロ",img:"SaoPaulo.jpg"},
            {key: 8,name: "リオデジャネイロ",img:"Rio.jpg"},
            {key: 9,name: "ブラジリア",img:"Brasilia.jpg"},
        ]
    },
    computed:{
        filterData: function(){
            let word = this.word
            let ary_data = this.ary_data
            let hit_data = this.hit_data
            let lists = []
            word = this.hiraToKata(word) //平仮名をカタカナに変換(methodsのケース3)
            if(word != ""){
                ary_data.filter(function(elem,idx){
                    if(elem.name.indexOf(word) !== -1){
                        lists.splice(idx,1,elem)
                    }
                })
                return lists; //検索結果が返される
            }
            return ary_data //初期に全値を出力したい場合はこのようにしておく
        }
    },
    //イベントメソッド
    methods:{
        //値のクリアはボタンが押されたときだけ反応すればよい(リアクティブに対応しない)
        clear: function(){
            this.word = '' //returnを使わない場合最終行が評価される
        },
        //画像のパスを読み込む(画像はキャッシュされると同じ画像を返してしまうため、イベントメソッドで制御する。methodsのケース2)
        imagePath: function(imgsrc){
            if(imgsrc != ''){
                return require(`@/assets/img/${imgsrc}`)
            }
        },
        //入力された平仮名をカタカナに変換する
        hiraToKata: function(word){
            return word.replace(/[\u3041-\u3096]/g, function(match) {
                chr = match.charCodeAt(0) + 0x60;
                return String.fromCharCode(chr);
            })
        }

    }
})
</script>
</body>

ここでは検索文字のクリアの場合と、画像の呼び出し、そして入力文字のカタカナ変換にmethodsプロパティを使っています。まず検索値のクリアを実行するにはボタンのクリックが必要ですが、クリックに際して@clickイベントは、あくまでクリックされた場合のみ処理が実行されればいいものです。そこでこのメソッドに@click(v-on:clickの略称)ディレクティブを記述しておき、そのメソッドclear内には検索文字を空白化しておきます(dataに値を返すのを忘れないで下さい)。

また、画像の呼び出しはassetsというものを使って制御していますが、ここでは難しいことは置いておいて、画像は値がキャッシュされてしまうので、次別の値を呼び出そうとしても同じ画像キャッシュを保持し続ける現象が起きてしまい、同じ画像しか表示されなくなってしまいます。そこで算出プロパティではなくメソッドを:src(v-bind:srcの略)に記述しておくことで画像のソースに対して値をバインドし、キャッシュを保持させることなく、それぞれの画像を表示できます。このように、メソッドは必ずしもv-onディレクティブ(イベント制御)上で起動するものとは限りません。

そして入力文字のカタカナ変換ですが、そのまま入力しても平仮名のままでは検索に引っかかるわけないので、各入力かなに対し、カタカナに変換する処理関数hiraToKataを算出プロパティfilteredList内で呼び出しています。そして結果を元の算出プロパティに返し、それを検索条件としているので、ひらがなを入力してもカナ検索ができます。

ですが、これだけだと入力文字が平仮名のままなので、もう少し改良を加えてみましょう。

■監視プロパティ(watch)

現在、かな検索で表示されるのは平仮名ですが、工夫すれば入力文字もカタカナで表示させることができます。それを不自然なく処理するために監視プロパティを使ってみましょう。ちょうどdataとcomputedの間に以下のメソッドを書き足してください。

    data:{
        },
    watch:{
        word:function(word){
            chr = this.hiraToKata(word)
            this.word = chr
        }
    },
   computed: {

これは監視プロパティというもので、名の通り、イベントそのものを監視するものです。今回はv-model="word"イベントそのものを監視しているので、このv-modelに動きがあった場合(computedの違いは内部の同期データを監視しているものではないということ)、内部でカナ変換メソッドhiraToKanaを呼び出し、変換されたカタカナを返す変数chrを返すようにしています。また、その受け皿として:valueには変数chrを代入しています(これを設定しないと値がクリアされません)。

※補足:こんな記述に注意

なお、次のような記述は警告が出ます(たまにやっているサイトを見ますが…)。

<input @change="overwork(sel)" v-model="sel">

v-modelで既に値を監視しているので、そこにメソッドを入れると想定外の動きを起こすことがあるからです。

React

さて、Reactですが、敢えて他のページと差別化を図るために(…というより2つ書くのが大変だし、他にこことか優秀なページがあるので…)、16.8で再現してみます。そして、それにしたがい、フックを活用しています。

react-lesson3.html
<script type="text/babel">
const { useState, useEffect } = React; //使用するフックの定義
const App =()=>{
    //変数定義
    const [word,setWord] = useState(""); //フックの使用(検索文字のバインド)
    const [searched ,setSearched] = useState([]); //フックの使用(検索結果のバインド)
    const ary_data = [
        {key: 1,name: "モスクワ",img:"Moscow.jpg"},
        {key: 2,name: "サンクトペテルブルク",img:"Sankt.jpg"},
        {key: 3,name: "エカテリンブルク",img:"Yekaterin.jpg"},
        {key: 4,name: "ムンバイ",img:"Mumbai.jpg"},
        {key: 5,name: "ベンガルール",img:"Bengaluru.jpg"},
        {key: 6,name: "コルカタ",img:"Kolkata.jpg"},
        {key: 7,name: "サンパウロ",img:"SaoPaulo.jpg"},
        {key: 8,name: "リオデジャネイロ",img:"Rio.jpg"},
        {key: 9,name: "ブラジリア",img:"Brasilia.jpg"},
    ];
    //ループ処理の分離
    const setList = searched.map((data,key)=>{
            return(
            <li key={data.key}>
                <dl>
                    <dt>{ data.name }</dt>
                    <dd ><img src={data.img}/></dd>
                </dl>
            </li>
            )
    })
    //useEffectフックによって、検索値がバインドされたときに処理が実行される
    useEffect( ()=>{
        const searched = ary_data.filter((item,key)=>{
            return item.name.search(word)!== -1;
        });
        setSearched(searched);
    },[word]);
    //検索文字のバインド
    const bindWord = (e)=>{
        let word = e.target.value
        word = word.replace(/[\u3041-\u3096]/g, function(match) {
            let chr = match.charCodeAt(0) + 0x60;
            return String.fromCharCode(chr);
        })
        setWord(word); //検索文字のバインド
    }
    //検索文字のクリア
    const clear = ()=>{
        let word = ''
        setWord(word);
    }
    //レンダリング
    return(
        <React.Fragment>
        <input type="text" onChange={bindWord} value={word}/>
        <p>検索結果{searched.length}</p>
        <button onClick={ clear }>文字のクリア</button>
        <ul>
        { setList }
        </ul>
        </React.Fragment>
    );
}
const elem = <App />;
ReactDOM.render(
    elem,
    document.getElementById("root")
);
</script>
</head>
<body>
    <div id="root"></div>
</body>

非常に複雑に見えますが、流れとしてはこうなっています。

  1. JSX内のsetListオブジェクトにリストの値を代入する。
  2. テキストに文字が入力されたら、bindWordメソッドが実行され、setWordメソッドによって検索値がバインドされる。
  3. 検索値に変化があったら、それをキャッチしたuseEffect(フックの一つ、後述)が働き、リストの値(ary_data)に対してマッチングを行いながら、検索値にマッチした値のみsearchedオブジェクトに格納される。
  4. 検索結果が格納されたsearchedはsetSearchedメソッドによってバインドされ、リストの値が更新される。
  5. もし、クリアボタンが押された場合は、検索値が初期化され、それに対しバインドが行われるため、リストは初期化される。

以上の流れになります。ここで鍵となり、そして16.8から新たに導入されたuseStateフックとuseEffectフックについて説明しようと思いますが、Vue.jsで言うところのv-modelディレクティブと算出プロパティのような働きだと認識しています。

フック(hook)

フックはReact16.8から導入された新機能で、日本語に直すと接続のことです。そして今までクラスオブジェクト依存していた記述に代わり関数で処理できるようになったことで、それに伴いよりデータの同期処理をより円滑に行うために開発された機能的なメソッドです(というよりコンストラクタやsetStateが普通だと使えなくなったので、その代替の手段です)。

●useState

useStateは実は演習1にも登場していましたが、全く触れませんでした。これはどういうものかというと、値の変更前後で同期をとるためのメソッドで、以下のような記述ルールがありますが、これを把握していないと引っかかります(前後の変数を代入するものではありません)。
const [バインドさせたい値,バインドさせるためのメソッド] = useState(初期値)
つまり、検索値を処理する場合、初期値は何もないのでuseState(””)と空白を代入し、1つ目の戻り値は検索値を格納するは変数word、それに対して2つめの戻り値に代入するのはそれをバインド処理するためのメソッドsetWordであるということです。同様に、検索結果に対しても同じように記述しますが、注意点として初期値がオブジェクトになる場合は引数にuseState([])と空オブジェクトを代入する必要があります。混乱しないためにも、奇を衒わずバインドを行うメソッド名はsetXxxxxXxxxxはバインドしたい値と対応)とした方が迷いが起こらずいいでしょう。

●useEffect

名の通りエフェクトを与えるフックで、バインドした値が変化したときに処理を行う、いわばVue.jsのv-modelと算出プロパティ(computed)の関係と同じようなものだと思えばいいでしょう。なお、公式サイトでは副作用と物々しいな言葉を使っていますが、どちらかといえば、連動の方がふさわしい気がします(effectにそのような意味はありませんので、相乗効果、波及といった意味合いでもいいでしょう)。

さて、今回の演習では検索結果を返す処理に対してこのuseEffectを使用しています。なぜなら、検索値をバインドさせても、それはあくまで検索値のバインドに過ぎないので、それだけでは検索結果までバインドできないからです。そこでこのuseEffectを使い、検索値をバインドさせ、その値に変化が与えられた時のみ、その連動で、検索結果も処理するようにしているわけです。そしてその検索結果もまた、useStateでバインド処理することで、随時検索結果をフィルタリングすることができます。

このuseEffectの記述方法ですが、このようになります。
useEffect(()=>{...元となる値によって連動させたい処理...},[連動させたい元となる値])

※注意点として元となる値は必ずオブジェクトに格納してください。もし、今回のように単発の値であっても、オブジェクトに格納しないと処理エラーが起きます。

演習4: データリストの追加、変更、削除

いわゆるCRUD(Create、Read、Update、Delete)と呼ばれる操作です。これに至っては今の所Vue.jsやReactのバインド(同期)が足かせになることもあり、I/Oのタイミングがわかりやすく、オブジェクト操作がしやすいjQuery+Ajaxの方が便利ではないかと考えている(PHPやRailsなどと合わせてDB操作の補助的な機能としてjQueryを重宝している理由です)部分ですが、これについても各フレームワークの癖も掴みながら解説していきたいと思います。

また、本業務も更に忙しくなってきているので、更新もご無沙汰になるかも知れませんが、宜しくお願い致します。

to be continued...

追伸:まさかここまで再生数と評価数が上がるとは思いませんでした。嬉しさと同時に大凡な記事は作れないな、という責任感と少しばかりの重圧も抱いております。至らないところもあるかも知れませんが、ご指導ご鞭撻の程を宜しくお願い致します。

なお、自分はこのように知らない言語は、知っている言語と知らないもの同士最低2セットを並行しながら学習していき、共通点と相違点を洗い出していくということをよくします。ですので、今回の場合はjQueryと並行させ、Vue.jsとReactを敢えて並行し、演習していくという手段をとっています。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした