588
646

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

バックエンドエンジニアのためのVue.js、React、Angular入門(2025改訂版)

Last updated at Posted at 2020-01-24

解説のバージョン(2025年3月時点)

  • Vue3.5
  • React19
  • Angular19

WEBシステム、WEBプログラム開発において昨今ではVue、React、AngularなどのJSフレームワークが主流となってきています。ただ、これらの活用は学習コストが高いといわれていました。その原因について、自分はフロントエンドありきで話が進みすぎていたからだと考えています。したがって、自分の投稿記事はバックエンド処理をメインにこなしてきたWEBシステムエンジニアに向けた、フォーム操作をメインに置いた半備忘録兼自分なりに解釈した解説となります。

ちなみに自分はサーバ構築からフロントエンド、バックエンドまでこなしているワンオペエンジニア(フリーランス、非正規雇用に非ず)です。それから自分は根っからの文系なので、公式チュートリアルの抽象的な説明(加えて、原本の英文ドキュメントに対し、日本語に慣れない人が和訳しているので)にどうしても慣れないというのもあったので、かなり独自解釈が多いかも知れません。

  • コンポーネント、ルーティング、スタイル周り、TypeScriptに関する続編も公開しています(編集が困難になるほど容量圧迫してたので、それぞれで独立させています)。

続編で一番見られているSvelte版(Svelte5、runeにも対応しました)

solidJSも鋭意作成中

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

その前に、vue.jsとReact、そしてAngularとはどういったもので、どんな意図で開発されたものかを知っておく必要があります。そして、その理念を知っていたら、各フレームワークの理屈もわかりやすいからです。

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

具体的にいえば、フォームに入力した値を転送後に表示させるだけの簡単な制御でも、JSフレームワークが存在しなかった頃は、数行の式を書いたりサーバサイドのプログラムとクライアントサイドのプログラムを並行させたりしていました(Ajaxもその技術の一つ)。それが、JSフレームワークが登場してから、これらの処理を数少ない記述で、しかも高速にこなせるようになっています。

それを最初に実現したのがGoogle開発によるAngularJSというライブラリ(2021年にサポート終了)で、これから派生した、あるいは改良したものが昨今の主流となっています。

それが冒頭に登場したVue(Vue.js)、React、Angular、それに後発のSvelteとSolidJSなどです。

※もともとはjQueryとの比較目的でしたが、昨今新たにjQueryを学習する機会は減っているので、2025年版では紹介対象外としています(jQueryも2024年になって遂に4が登場しましたが…)。

Vue.js

Vue.js(ヴュージェイエス、以下Vueと表記)はもともとGoogleが開発したデータ同期用ライブラリAngularJS開発者の一人、エヴァン・ヨーが、個人で開発を始めたJSフレームワークです。Vueは柔軟に開発できるというコンセプトで生み出されたプログレッシブ・フレームワークという概念を持っており、フォーム操作の円滑化のために開発されました。そのため、小規模の開発に向いた柔軟な利用も可能です。そのVueとは一言でどんな技術かというと

アプリケーション内のインラインテンプレートに対して、Vueディレクティブという魔法をかけ、多種多様な呪文(プロパティ)でデータを加工していく

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

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

この理念を覚えておけば、今後の学習にも役立ちます。また、既存の書き方をoptions APIと呼びますが、Vue3よりcomposition API(コンポジションAPI)を用いた記法が可能となっています。またVue3.2からはボイラープレート(冗長な記述)を除去したsetup記法が主流となっており、公式はsetupを推奨しています。

また、昨今ではバックエンド用フレームワーク(LaravelやRailsなど)のフロントエンドツールとして選択されることも多くなっています(Laravelの場合はVueとパートナーシップ提携しています)。

React

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

html内に魔法結社(JSXの製造拠点、つまりはコンポーネント)を作り、そこでデータをフックという秘薬で調合しながらリアクティブな部品(JSX)を錬金する。

こういうものです。つまり、Vue.jsだといわゆるリアクティブな部品(Vueディレクティブ)自体はhtml上に仕掛けられており、それをコンポーネント上の各種プロパティやメソッドで加工していくのに対し、Reactの場合はあくまでJavaScriptが基本であり、その延長線上に、リアクティブな処理を実現させるために必要なフック(処理用ライブラリ)などがあり、ダイレクトに変数を加工していきます。したがって、リアクティブな処理を実施する部品はJSXにはなく、コンポーネント上で作成されることになり、内部だけでデータ処理するので開発を分担しやすく小~中規模のシステムに向いています。また、状態管理が可能なフックも十分に用意しているので、きめ細かいステート管理が必要なデータ処理に適しています。

あと、コンポーネントの記法もいろいろあって、しかもしょっちゅう変更されていた(2025年現在は関数コンポーネントでの記述が主流なので、新たに学習する場合は関数コンポーネントだけ意識するといいでしょう。

Angular

Angular(アンギュラー)はGoogle社が開発したAngularJSでの失敗と課題を教訓に、その原因となったソースの汚れを解消させたものです。そしてvue、Reactのようなライブラリとは違い、RubyのRailsやPHPのLaravel、PythonのDjangoのような、それ一つでパッケージとなっているフルスタックフレームワークです。そのロジックはといえば

パッケージ自体がAngularという魔法世界(フレームワーク)であり、その中でバインディングという魔法の扉でデータをやりとりする

このようなものです。したがって、そのパッケージ内では自由自在にAngularの技術を利用できます(VueやReactのように、どこからどこまでという範囲指定が不要)。ですが、前述したようにソースの汚れを反省して作ったフレームワークなので、データや部品、処理は各アーキテクチャ内で分担されており、データの同期処理もバインディングというものでやりとりします。つまり、上記2つと比較するといろいろなメソッドでデータの同期を取っていくというよりは、サーバサイドとクライアントサイドの間で、Ajaxを使ってやりとりしているようなものです。ただ、基本はAngularJS時代とそこまで変わっていないためにあまり難しくなく、どちらかというとVue.jsに(特にoptionsAPI時代のVue.js)近いですが、JavaScriptでの記述法がそのまま応用できるためデータ自体のやりとりの理屈は解りやすいので、フレームワーク開発に慣れているバックエンドエンジニアなら、上記2つより学習は楽でTypeScriptの癖さえ押さえておけば、思ったよりは簡単にプログラムを作れます。また、大規模開発にも対応できます(ただ、普通に軽量システムに用いることも可能ですし、敬遠の原因の一つともいわれるRxJSも、ルーティング(続編の第6章参照)でデータを受け渡しするまでは覚える必要ありません。スペックが揃っているので、小規模~中規模システムには少しばかり性能を持て余していた感があったのですが、スタンドアロンコンポーネントとsignalを標準実装してからは、小規模なアプリケーションにも適応するようになりました。

また、TypeScriptでの記述が基本となっているのですが、JavaScriptと互換性を持っている上に、Angularはあまり変数を独自の制限で縛ってしまわないため、最初のうちはそこまで難しく考えることないと思います。

※本記事でAngularJSは採り上げません。よく勘違いしている記事を見かけますがAngularJSとAngularは全く別物で、互換性は全くない上にAngularJSは2021年12月を以てサポート終了しています。

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

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

したがって、自分はjQueryで操作してきたフォーム操作をVue、React、Angularではどう記述するのかを重点において解説してきました(jQueryの操作がわかりやすかったのは、英文法のように主述関係がはっきりしていたからです)。

また、かつてはライブラリとして使用することを前提としていた部分もあったために、CDN(contents derivery network…要はサーバを立ち上げなくても利用可能な、webを通して使用するライブラリ)での記述もできましたが、Vueは応用性の薄い記述に、Reactもなるべく非推奨となっているので、VueはVueクライアント(Vue CLI)、ReactはReact Nativeで演習環境を作成しています(2025年現在においては、いずれもVite対応なのでそれを利用して簡単に開発環境を構築できます)。

また現在では、それぞれ公式サイトにプレイグラウンドがあるので、そこで実践してみてはいかがでしょうか。

※アプリケーションの仕組みにおける詳細については、各続編の演習5と6で詳しく紹介します。

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

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

Vueで再現

Vueで記述すると以下のようになり、v-modelで紐づけたフォームの値が変数mesに表示されます。setup記法はscriptタグにプロパティを追記するだけで実現可能です。また、templateタグで囲まれた部分はインラインテンプレートといい、そのタグに囲まれたhtml部分をエレメントといいます。このエレメントはVue3から複数タグの記述が可能です。

lesson1.vue
<script setup>
import { ref } from 'vue'; //外部からrefをインポート
const mes = ref("") //refはプリミティブな変数を展開するために必要
console.log(mes.value) //スクリプト上で値を展開する場合はvalueを付与すること
</script>

<template>
	<input type="text" v-model="mes">
	<p>入力された文字<span>{{ mes }}</span></p>
</template>

このようにsetup記法では、非常に記述がスリムかつ、シンプルになりました。

ref

単一のデータをリアクティブ(簡単にいえば同期対象)にするメソッドで、外部からインポートして使用します。注意点として、refで取得した値はスクリプト上で展開する場合、hoge.valueとする必要があります。この変数を同期させることをリアクティブと呼んでいます。

※refによってリアクティブになった変数は、実は状態管理用のステータスも保持しています。なので変数そのものの値を展開する場合hoge.valueとしなければいけないのです。

一方、テンプレート上は変数しか展開できません。なので、逐一hoge.valueとせず、hogeとすればマスタッシュや後述するv-bindキー上に変数を展開できます。

v-modelディレクティブ

refで指定した変数に対し、フォーム上の値を同期させるためのディレクティブで、これを用いれば、すぐに同期対象の値を取得できます。

const mes = ref(); //値を代入する変数を準備
v-model="mes" //その変数を紐づけるための手段の一つ

※このv-modelと似た働きをするのがv-bindディレクティブというもので、v-bind:プロパティ名="変数名"と記述します。これを使えば、プロパティをリアルタイムに同期をとることができます。

マスタッシュ

テンプレート上に値を展開する二重括弧の記号はマスタッシュといい{{ hoge }}と表記されます(mustacheとは口髭のこと)。注意点は先ほど説明した通りで、refを通した場合で、スクリプト上では状態も保持しているので、値を表示するときはhoge.valueなのに対し、マスタッシュ上では値を表示するだけの働きしか持っていないのでvalueの付与は不要です。

※後述するreactiveの場合は、スクリプト、テンプレートのいずれでも同じ記法となります。

Reactで再現

次はReactで記述してみます。Reactはスクリプト上の計算式をJSX上の変数に返すという形をとることもでき、最も少ない字数の書き方は以下のようになります。

ReactはVueに較べて習得が難しい!なんて声もよく聞きますが、ある程度JavaScriptの基本さえ押さえてあれば、そんなことはありません。Reactは前述したように、あくまでJavaScriptの延長線という理念のフレームワークなので、JavaScriptだけでは再現困難な技術(ここでは変数の同期)を、後述するフックというReact独自の処理用ライブラリで補っているだけです(逆に、最初で躓いている学習者はJavaScriptの基礎ができていない)。

jsx.lesson1.jsx
import { useState } from "react"; //フックはReactからインポートする
function Lesson(){
	//処理部分
	const [mes,setMes] = useState(""); //バインディング処理
	//レンダー部分
	return(
		<div>
			<input onChange={(e)=>setMes(e.target.value)} />
			<p>入力された文字<span>{ mes }</span></p>
		</div>
	);
}
export default Lesson

JSX

Reactはテンプレートではなく、JSXというhtmlに類似した言語を返します。また、このJSXは共通する以下の決まりがあります。

  • 単一の親要素しか生成してはいけない(中に子要素、孫要素があるのは問題ない)
    もし、複数のタグを使いたい場合は、<>という空タグでラッピングするのが昨今のセオリーです。
  • <input>タグのようにそれ一つで完結するタグは必ず<input />のようにスラッシュを入れること。これを入れないとJSXがどこまで適用したらいいのかわからずにエラーが起きます。これはコンポーネントの<App />などでも同様です。
  • JSXは必ず丸括弧(...)で囲って値を埋め込むこと。これは次章のmapメソッドでも活用します。
  • プロパティに特殊な記述ルールがある
    たとえば、classプロパティはclassが予約語であるため、ReactではclassNameと記述する必要があります。

もっと詳しく説明するとJSXで作成しているのは、あくまで同期操作を実現するためのコンポーネントと呼ばれる部品です。なのでreturnはコンポーネント全体に値を返すのに対し、丸括弧で囲まれた部分はあくまでJSXを返しているだけです(以前はこの部分にもreturnが必要だったりしたため、学習者の混乱を招いたりしていました)。

useState

Reactにおいてすべての基本となるフックです。このuseStateで設定した変数がリアクティブ(同期対象)になります。フックはReactを操作する上で極めて重要なので、演習3で詳しく紹介します。

const [hoge,setHoge] = useState(初期値)

●イベント

イベントはonHogeとキャメルケースで記述し、後述するオブジェクトリテラルでラッピングします。また、その関数は以下のように、式を展開しないと無限ループに陥ります。

	<input onChange={(e)=>setMes(e.target.value)} />

eは便宜上の引数(eventの略称で、別になんでも構わない)で、e.target.valueとすることで、任意のフォームにおける値を取得することができます(これはReact独自の公式ではなく、JavaScriptそのものの公式で、こういう名称を指定しない関数をインラインメソッドという)。この取得した値を、前述したuseStateフックのセッタに代入しています。

※今回はイベントを制御するための引数を使っているので迷うことはありませんが、インラインメソッドを使わずダイレクトに、

const keyinput = (e)=>{
   setMes(e.target.value)
}
	<input onChange={keyinput} />{/**/}

などと記述してしまうと無限ループに陥る危険があるので、エラーとなります(演習4でおさらいします)。

◆Angular

同じ動作をAngularでも再現してみます。Angularもスタンドアロンコンポーネントになってからは、単一コンポーネントを制御するのが随分と楽になりました。Angularで大事なのは、Vue、Reactと異なり、コンポーネントファイルに対し、デコレータ、テンプレート、クラスといった複数の構成要素(これをアーキテクチャと呼んでいる)を持っている点です。

デコレータ

Angularはデコレータというもので、コンポーネントに対して、処理を円滑に実行するためプロパティを設定します。コンポーネントの場合@Componentデコレータで、親コンポーネントの名称指定、外部ファイルのインポートやテンプレート、スタイルの設定を行います。また、デコレータを使用するには各種モジュールを、事前に外部からインポートしておく必要があります。

lesson.component.ts
import { Component } from '@angular/core'; //Componentデコレータをインポート
import {FormsModule } from '@angular/forms'; 

//デコレータ
@Component({
  selector: 'app-lesson',
  imports:[FormsModule], //インポートを忘れない
  template: `
	<input type="text" [(ngModel)]="mes">
	<p>入力された文字<span>{{ mes }}</span></p>  
  `,
})

//コンポーネントクラス
export class LessonComponent{
	mes = '' //テンプレートで使用する変数を事前に設定する
}

◆バインディング

Angularにおける同期操作を司るものはバインディングというデータやりとりのための扉です。[(ngModel)]は双方向バインディングといって、これ一つでVueのv-modelと同じように、フォームの値に対し同期制御ができます。

※後述する[hoge]はプロパティバインディングといって、値を受け取る側、(hoge)はイベントバインディングといって、値を受け渡す側となります([(hoge)]はその2つの働きを合体させたものです)。

※また、この双方向バインディングはFormsModuleを使用するので、imports[FormsModule]と、@Componentsデコレータに対する使用オプション記述を忘れないようにしましょう。

◆テンプレート

基本はtemplateURLプロパティが設定され、外部ファイルから呼び出すようになっていますが、短いテンプレートの場合は、templateとプロパティを書き換え、内部に直接記述した方が手っ取り早いです。その際、テンプレートはバッククォート(`)で区切る決まりがあります。

また、テンプレート内の{{ }}という二重ブラケットは、Vueのように間に変数やコールバック関数を出力できます。今回もmesから値が出力されています。その際、変数は必ずコンポーネントクラス上で定義しておく必要があります。

演習2:プルダウンメニュー(ループ処理)

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

Vue

Vueはループ処理用のディレクティブが存在します。ループ処理をVueで記述するとこうなります。

lesson2.vue
<script setup>
import { ref,reactive } from 'vue';
const sel = ref("");
const state = reactive({ary_data :[
	{id:0,name: "React"},
	{id:1,name: "Angular"},
	{id:2,name: "Vue"},
	{id:3,name: "Svelte"},
	{id:4,name: "solidJS"},
	{id:5,name: "Lit"},
]})
</script>

<template>
	<select v-model="sel">
		<option value="">選択</option>
			<option v-for="(data,idx) in state.ary_data" :value="data.name" :key="idx">{{ data.name }} 
		</option>
	</select>
	<p>選択された値:<span id="txt" >{{ sel }}</span></p>
</template>

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

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

ちなみに、ループさせる際には一意性(他と重複しないこと)を保つために:key(v-bind:keyの省略形。後述)が必須となり、これを忘れると警告が表示されます。

■Vueディレクティブの省略記法

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

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

reactive

変数をリアクティブ制御するメソッドは、reactiveというものもあり、これは一まとまりのデータをproxy制御によってリアクティブにしてしまうというものです。したがって、このreactiveで展開する場合、value指定は不要ですが、出力用のオブジェクト変数から展開する必要があります(公式ではstateに統一していますが、任意の変数を利用できます)。

React

同じようにプルダウンメニューの生成をReactでも記述してみます。ReactはJavaScriptの記法をそのまま使用しますが、JSXならではのルールがあります。

lesson2.jsx
import { useState } from "react"; //ローカルの場合で、useStateを使用するための定義
function Lesson(){
	//変数定義
	const [sel,setMenu] = useState(true);
	const ary_data = [
		{id: 1,name: "React"},
		{id: 2,name: "Angular"},
		{id: 3,name: "Vue"},
		{id: 4,name: "Svelte"},
		{id: 5,name: "solidJS"},
		{id: 6,name: "Lit"},
	];
	//レンダリング
	return(
		<>
		<select onChange={(e)=>setMenu(e.target.value )}>
			<option>選択</option>
			{ ary_data.map((data,idx)=>(
                <>
        		<option key={idx} value={data.name}>{data.name}</option>
                </>
        	))}
		</select>
		<p>選択された値:<span>{sel}</span></p>
		</>
	);
}
export default Lesson

Reactが採用しているJSXは、JavaScriptの延長線で記述できることを売りとしています。そのため、ループ制御における特有のコントロールフローといった部品はなく、map関数を用います。後は、valueプロパティやテキスト値にidやnameを配列順に代入していきます。

※ただしJSX上でのmap展開において、Vue同様にkeyプロパティを忘れると警告が出ます。必ず一意の値(他と被らないもの)を指定するようにしてください。

●オブジェクトリテラル

Reactでは、JSXと変数、あるいは計算式などを区別するために、オブジェクトリテラルという、{ ... } で記述される波括弧を用います。この内部に出力されるものはあくまで値です。ただ、メソッドなどもオブジェクト関数として戻り値を指定できるので、メソッドごと代入できたりします。

●カスタムコンポーネントによる記述

前述した通り、オブジェクトリテラルは戻り値を持つメソッドの代入が可能と説明しました。したがって、以下のような記述も可能で、かつては主流とされていた記法でした。ちなみに、このように独立して部品化したコンポーネントをカスタムコンポーネントと呼びます。

//設定処理 ary_dataを順々に展開し、data変数に格納。該当するプロパティをそれぞれ当てはめていく。
	const setList = ary_data.map((data,idx)=>(
		<option key={idx} value={data.id}>{data.name}</option>
	))
    	return(
		<>
		<select onChange={(e)=>setMenu(e.target.value )}>
			<option>選択</option>
			{ setList }
		</select>
		<p>選択された値:<span>{sel}</span></p>
		</>
	);

Angular

Angularでもプルダウンを作成してみます。Angular17からビルトインコントロールフローが採用されたので、それを使用しています。

lesson.component.ts
import { Component,signal } from '@angular/core';
import {FormsModule } from '@angular/forms';
const data = [
	{id: 1,name: "React"},
	{id: 2,name: "Angular"},
	{id: 3,name: "Vue"},
	{id: 4,name: "Svelte"},
	{id: 5,name: "Solid"},
	{id: 6,name: "Lit"},
]
@Component({
  selector: 'app-lesson',
  imports:[FormsModule],
  template: `
		<select [(ngModel)]="sel">
			<option>選択</option>
			@for(data of ary_data ; let i = $index ;track i){
				<option [value]="data.name">{{data.name}}</option>
			}
		</select>
		 <p>選択された値:<span>{{sel}}</span></p>  `,
})

export class LessonComponent{
	ary_data = data
	sel = ''
}

プルダウンの場合はoptionタグのvalueプロパティをngModelが紐づけ、それを展開すると取得できます。また、ループ式に関してはAngular17から推奨記法が大きく変化しました。

◆プロパティバインディング

optionタグのvalueに[value]="data.key"としているのはプロパティバインディングといって、データからビューへの偏方向を操作するために用います。簡単に言えばテンプレートから処理クラスへの入口オンリーのバインディングになります(入口オンリーがあるなら当然出口オンリーのバインディングもあります。それが後述するイベントバインディングです)。

◆ビルトインコントロールフロー

Angular17から利用可能となった埋め込み構文です。今回はループ文に@forを使用していますが、@forにはtrackが必須となっています。trackは一意な値を指定するのがセオリーなので、今回はidを利用しています。

@for( 展開したい値 of 展開対象のオブジェクト; track 一意となる値){
   //ループ対象のテンプレート
}

これが基本文です。インデックスを使用したい場合は以下のように$indexから取得します(iは任意の変数)。ループ文で、一意になる値が存在しない場合は、以下の記述が必須となります。

@for( 展開したい値 of 展開対象のオブジェクト; let i = $index; track i){
}

※ビルトインコントロールフローはif文などもあります(後述)。

演習3:検索フォーム(値の連動)

今回は、テキストボックスに入力された文字をもとに、リスト一覧から該当する要素を出力するという機能をそれぞれVue、ReactそしてAngularで実装してみます。なお、これがよくある検索システムにおける基本の動きとなり、だんだんデータの動きが複雑になってきます。

Vue

Vueで記述すると、以下のような記述になります。今回は算出プロパティ、メソッド、そして監視プロパティが登場します。

lesson3.vue
<script setup>
import {ref,reactive,computed,watch } from 'vue';
const word = ref("");
const filterData = ref("")
let items = reactive({
      ary_data:[
        {id: 1,name: "モスクワ",img:"Moscow.jpg"},
        {id: 2,name: "サンクトペテルブルク",img:"Sankt.jpg"},
        {id: 3,name: "エカテリンブルク",img:"Yekaterin.jpg"},
        {id: 4,name: "ムンバイ",img:"Mumbai.jpg"},
        {id: 5,name: "ベンガルール",img:"Bengaluru.jpg"},
        {id: 6,name: "コルカタ",img:"Kolkata.jpg"},
        {id: 7,name: "サンパウロ",img:"SaoPaulo.jpg"},
        {id: 8,name: "リオデジャネイロ",img:"Rio.jpg"},
        {id: 9,name: "ブラジリア",img:"Brasilia.jpg"},
      ]
});
//算出プロパティ
const filter_data = computed(()=>{
	return  items.ary_data.filter( (item) =>{
		return item.name.indexOf(word.value) !== -1 && word.value != ''
	})
})
//メソッド(関数として記述する)
//値のクリアはボタンが押されたときだけ反応すればよい(リアクティブに対応しない。ケース1)
const clear = ()=>{
  console.log("clear")
  word.value = '' //returnを使わない場合最終行が評価される
}
//画像のパスを読み込む(画像はキャッシュされると同じ画像を返してしまうため、メソッドで制御する。ケース2)
const imagePath = (imgsrc)=>{
  if(imgsrc != ''){
	return `./image/${imgsrc}`
  }
}
//監視プロパティ
watch([word],()=>{
  const chr = hiraToKata(word.value)
  word.value = chr
})
//入力された平仮名をカタカナに変換する(システム内部で処理する。ケース3)
const hiraToKata = (chr)=>{
  return chr.replace(/[\u3041-\u3096]/g, function(match) {
	chr = match.charCodeAt(0) + 0x60;
	return String.fromCharCode(chr);
  })
}
</script>

<template>
<input type="text" v-model="word" />
<p>検索結果{{ filter_data.length }}</p>
<button @click="clear">文字のクリア</button><!-- clearはmethodsのケース1-->
<ul>
	<li v-for="(data,idx) in filter_data" :key="idx">
		<dl>
			<dt>{{ data.name }}</dt>
			<dd ><img :src="imagePath(data.img)"></dd>
		</dl>
	</li>
</ul>
</template>

■算出プロパティ(computed)

ここで初めて算出プロパティが登場します。

算出プロパティを難しく考える学習者が多いようですが、簡単に言えば、再計算処理が必要な変数の受け皿で、それをcomputedという機能で隔離しているだけです(なので、当然のようにrefやreactveで定義した変数と名称を被らせることはできませんし、定義変数への再代入もルール違反です)。

またv-modelなどでリアクティブに依存している値が更新された場合、即座にその更新データをキャッチして処理を行い、データの同期をとることができます。裏返せば、値が不変の場合は一切反応しないという優れた性能を持っています。

その戻り値はv-forディレクティブのほかに、前述した各種v-bindディレクティブなどに紐づけることもできます。

具体的には検索用のテキストボックスはv-modelディレクティブで値をバインドしているため、逐次検索文字が変わるごとにfilterDataプロパティが実行され、その結果を返します。当該メソッド内ではindexOf()メソッドによって値のマッチングを調べているので-1(検索結果なし)以外は反応するようにし、それに従いspliceメソッドによって検索結果を新たなlistオブジェクトに代入するようにしています。そしてその結果を変数として受け取り、v-forディレクティブの初期値に設定し、値をループさせています。

ちなみに、今回の場合は一番基本的な制御となり、ゲッタとセッタを設定していません(この2つについては後述します)。また、この算出プロパティは値がキャッシュされるので、それによって高速な値のリアクティブ処理が可能になっています(それによる弊害もあるのですが、それは次の説明で紹介します)。

■メソッド(methods)

算出プロパティに対して、メソッドというのものもあり、option APIの頃はプロパティを分別していました(今は普通の関数と同じ扱い)。Vueでは算出プロパティで処理できるものが多いのですが、メソッドは以下の場合に用いることが多いです。

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

具体的な例を挙げましょう。

lesson3.vue
<script setup lang="ts">
/*中略*/
//メソッド(関数として記述するだけ)
//値のクリアはボタンが押されたときだけ反応すればよい(リアクティブ制御が不要な処理。ケース1)
const clear = ()=>{
  word.value = '' //returnを使わない場合最終行が評価される
}
//画像のパスを読み込む(画像はキャッシュされると同じ画像を返してしまうため、メソッドで制御する。ケース2)
const imagePath = (imgsrc)=>{
  if(imgsrc != ''){
	return `./image/${imgsrc}`
  }
}
//入力された平仮名をカタカナに変換する(システム内部での再計算処理。ケース3)
watch([word],()=>{
  const chr = hiraToKata(word.value) //ここから呼び出している
  word.value = chr
})
const hiraToKata = (chr)=>{
  return chr.replace(/[\u3041-\u3096]/g, function(match) {
	chr = match.charCodeAt(0) + 0x60;
	return String.fromCharCode(chr);
  })
}
</script>

ここでメソッドを使用しているのは以下の3箇所です。

  • (1)検索文字のクリア
  • (2)画像の呼び出し
  • (3)入力文字のカタカナ変換

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

(2)画像の呼び出しはブラウザによって値がキャッシュされてしまうので、次別の値を呼び出そうとしても同じ画像キャッシュを保持する現象が起きてしまい、更新できなくなってしまいます。そこで算出プロパティではなくメソッドを:src(v-bind:srcの略)に記述しておくことで画像のソースに対して値をバインドし、キャッシュを保持させることなく、それぞれの画像を表示できます。

(3)入力文字のカタカナ変換はプログラム内の内部関数として使用しています。検索文字は、そのまま入力しても平仮名のままなので、カタカナに変換する処理関数hiraToKataを算出プロパティfilteredList内で、新たに内部処理用に呼び出しています。そして結果を元の算出プロパティに返し、それを検索条件としているので、ひらがなを入力してもカナ検索ができます。

■監視プロパティ

これもよく算出プロパティとの違いがわからない、ややこしいという意見を目にします。

これは名の通り、イベントそのものを監視する機能です。算出プロパティが戻り値に対し制御を隔離しているのに対し、監視プロパティの制御対象は引数を持ったイベントそのもの、つまり処理対象はイベント上の引数(ただし、変数を監視対象にはできない)になります。

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

監視プロパティは処理上の無駄が多いらしいので、どっちでも対応できる場合、公式には算出プロパティを推奨しています。しかし、今回のように引数がそのままでは都合が悪い場合(ひらがなを引数から持ってきても、海外の地名がヒットするわけがないので)などは、監視プロパティの方が効果的です。

vue
//入力された平仮名をカタカナに変換する
watch([word],()=>{
  const chr = hiraToKata(word.value)
  word.value = chr
})

また、複数のイベントを監視したい場合は監視対象をオブジェクト化すれば対応できます。

vue
    watch([イベント1,イベント2],()=>{
        //処理を行う
    })

※監視プロパティはほかに、値更新の有無にかかわらず、再代入の際に必ず処理を施すwatchEffectなどもあります。

■v-ifとv-else

ここで、エレメント部分をこのように書き換えてみてください。

     <div id="app">
        <input type="text" v-model="word">
        <p v-if="filterData.length > 0">検索結果{{filterData.length}}件</p>
				<p v-else>検索結果なし</p>
        <ul>
            <li v-for="data in filterData">{{ data.name }}</li>
        </ul>
    </div>

このv-if文とv-else文は同一エレメント内で分岐処理を行いたい場合に用います。ここでは判定文として検索結果を返しており、filter.lengthで検索件数が表示されるので、それが存在しない場合はv-else以下のタグが表示されます。

ただ、範囲としては単一のタグとなってしまうので、複数のタグを分岐させたい場合はtemplateタグが役立ちます。

<template v-if="filterData.length > 0">
    <p>検索結果{{filterData.length}}件</p>
</template>
<template v-else>
    <p>検索結果なし</p>
    <p>※全角カナを入力してください</p>
</template>

React

Reactで同じ処理を記述してみます。それにしたがい、今回は新たにuseEffectフックを使用します。

lesson.jsx
import { useState,useEffect } from "react";
function Lesson(){
	//変数定義
	const [word,setWord] = useState(""); //フックの使用(検索文字のリアクティブ制御)
	const [searched ,setSearched] = useState([]); //フックの使用(検索文字のエフェクト制御)
	const ary_data = [
		{id: 1,name: "モスクワ",img:"Moscow.jpg"},
		{id: 2,name: "サンクトペテルブルク",img:"Sankt.jpg"},
		{id: 3,name: "エカテリンブルク",img:"Yekaterin.jpg"},
		{id: 4,name: "ムンバイ",img:"Mumbai.jpg"},
		{id: 5,name: "ベンガルール",img:"Bengaluru.jpg"},
		{id: 6,name: "コルカタ",img:"Kolkata.jpg"},
		{id: 7,name: "サンパウロ",img:"SaoPaulo.jpg"},
		{id: 8,name: "リオデジャネイロ",img:"Rio.jpg"},
		{id: 9,name: "ブラジリア",img:"Brasilia.jpg"},
	];
	//ループ処理の分離
	const setList = searched.map((data,idx)=>{
			return(
			<li key={idx}>
				<dl>
					<dt>{ data.name }</dt>
					<dd ><img src={data.img}/></dd>
				</dl>
			</li>
			)
	})
	//useEffectフックによって、検索値がバインドされたときに処理が実行される
	useEffect( ()=>{
		const searched = ary_data.filter((item,idx)=>{
			return item.name.search(word)!== -1 && word
		});
		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(
		<>
		<input type="text" onChange={bindWord} value={word}/>
		<p>検索結果{searched.length}</p>
		<button onClick={ clear }>文字のクリア</button>
		<ul>
		{ setList }
		</ul>
		</>
	);
}
export default Lesson

非常に複雑に見えますが、処理の動きとしては以下のようになっています。

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

以上の流れになります。ここで鍵となり、そして関数コンポーネントから新たに導入されたuseStateフックとuseEffectフックについて説明しようと思います。

●フック(hooks)

フックはReact16.8、※関数コンポーネント制御から導入された新機能で、日本語に直すと「接続」のことです。具体的には変数に対し、各種処理を実施するためのライブラリ群だと認識しておけばいいでしょう。

※ライブラリなので、使用には各種呼び出しが必要です。

●useState

useStateは実は演習1にも登場していましたが、あまり触れませんでした。これはどういうものかというと、データの同期をとるためのメソッドで、以下のような記述ルールがありますが、これを把握していないと引っかかります(前後の変数を代入するものではありません)。

const [検索値,セッタ関数] = useState(初期値)

つまり、検索値を処理する場合、今回、初期値は何も指定しないのでuseState("")と空白を代入し、1つ目の戻り値は検索値を格納するのは変数word、それに対して2つめの戻り値に代入するのはそれをリアクティブ処理するためのセッタ関数、setWordとなります。同様に、検索結果に対しても同じように記述しますが、注意点として初期値がオブジェクトになる場合は引数にuseState([])と初期値(今回は空オブジェクト)を代入する必要があります。また、セッタ関数については、奇を衒わずリアクティブ制御を行うメソッド名はsetXxxxxXxxxxは検索値と対応)とした方が迷いも起こらずいいでしょう。

※このuseStateの戻り値はJavascriptの分割代入です。分割代入というのは[元の値,追加したい値]とすることで、オブジェクトの値を分解して、個別の変数を戻り値に代入していくものなので、useStateメソッドが実行される度に、オブジェクトの中身を分解し、更新された差分データを処理する仕組みとなっています。

●useEffect

名の通りエフェクトを与えるフックで、リアクティブ制御した値が変化したときに処理を行うもので、第二引数に監視対象の値を代入しておきます。なお、公式サイトではeffectを副作用と物々しげな(日本語では問題解決の方法によって起きた、新たな弊害を表すマイナスの言葉)和訳を使っていたため、それが一般化してしまっていましたが、React19の説明からはエフェクトと言葉を改めていました。

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

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

※フックは他に頻用されるものとしてuseContextフック、useReducerフック、useCallbackフック、useMemoフック、useRefフックなどがあります(これらが関数コンポーネント初期から登場した7つのフックです)が、これらの使い方は続編で紹介します。

●Reactで分岐処理する場合

ReactはVue、Angularのようにif用の部品を埋め込んだりはできないのですが、大きく分類して3種類の方法があるようです。

1.関数コンポーネント
2.即時関数(説明は省略)
3.三項演算子

※即時関数は拡張性がなく見づらい上、使用している人もほとんどないので説明を省略します。

●関数コンポーネントで分岐

react-sample3func.tml
//前略
    //ループ処理の分離
    const setList = searched.map((data,key)=>{
            return(
            <li key={data.key}>
                <dl>
                    <dt>{ data.name }</dt>
                    <dd ><img src={data.img}/></dd>
                </dl>
            </li>
            )
    })
		//分岐処理
		const Result = ()=>{
			let result
			if(searched.length > 0){
				result = <p>検索結果{searched.length}件</p>
			}else{
				result = <p>検索結果なし</p>
			}
			return result
		}
		
//中略
    //レンダリング
    return(
        <React.Fragment>
        <input type="text" onChange={bindWord} value={word}/>
        <Result />
        <button onClick={ clear }>文字のクリア</button>
        <ul>
        { setList }
        </ul>
        </React.Fragment>
    );

//後略

JSX内に<Result />という見慣れないタグが描かれていますが、実はこれもコンポーネントの一種でカスタムコンポーネントといいます。そして、カスタムコンポーネントはJSXで再描写が行われる毎に分岐処理が行われるので、そこにはResultメソッド内で処理された変数resultが随時返されることになります。

ただ、入れ子にならない場合は次に説明する記述法の方が楽です。

●三項演算子で分岐

昨今では、三項演算子での表記が主流となっています。この三項演算子は**複数行に跨っていても使用可能(ただし、コンポーネントが複数になる場合は空タグでラップすること)**です。またReact17から埋め込みのJSXに対してreturn記述が不要になったので、記述もすっきりしました。

※JSXにコメントを付与したい場合オブジェクトリテラル内に/コメント/とします。

//前略
    //レンダリング
    return(
        <>
        <input type="text" onChange={bindWord} value={word}/>
        { searched.length > 0? 
        (<>
        {/*肯定の処理*/}
        </>)
        :
         (<>
        {/*否定の処理*/}
        </>)
        }
        </ul>
        </>
    );
//後略

●(参考)&&接続

※なお、if判定がtrueの場合のみ、&&で接続することもできます(trueといってもbool型でないと評価されないというものではなく、要は条件一致の場合のみ処理が可能というものです)

   return(
        <>
        <input type="text" onChange={bindWord} value={word}/>
        <p>検索結果{searched.length}</p>
	{searched.length == 0 && <p>別の検索条件でやり直してください</p>}
        <button onClick={ clear }>文字のクリア</button>
        <ul>
        { setList }
        </ul>
        </>
    );

Angular

では、これをAngularで記述してみます。Angularはスタンドアロンコンポーネントになってから、単一コンポーネントでの同期制御が必須となりました。その処理を手助けしてくれるのが、Angular16から導入されたsignalです。

lesson3.component.ts
import { Component,signal } from '@angular/core';
import {FormsModule } from '@angular/forms';
export type Item={
	id: number,
	name: string,
	img: string,
}
export interface Items{
	items:Item[]
}
const items = [
	{id: 1,name: "モスクワ",img:"Moscow.jpg"},
	{id: 2,name: "サンクトペテルブルク",img:"Sankt.jpg"},
	{id: 3,name: "エカテリンブルク",img:"Yekaterin.jpg"},
	{id: 4,name: "ムンバイ",img:"Mumbai.jpg"},
	{id: 5,name: "ベンガルール",img:"Bengaluru.jpg"},
	{id: 6,name: "コルカタ",img:"Kolkata.jpg"},
	{id: 7,name: "サンパウロ",img:"SaoPaulo.jpg"},
	{id: 8,name: "リオデジャネイロ",img:"Rio.jpg"},
	{id: 9,name: "ブラジリア",img:"Brasilia.jpg"},
];
@Component({
  selector: 'app-lesson',
  imports:[FormsModule],
  template: `
	<div id="app">
		<input type="text" (input)="filterData($event)" [value]="word()" />
		@if(lists().length > 0){
			<p>検索結果{{lists().length}}件</p>
		}@else if(lists().length == 0 && word() != ''){
			<p>検索結果なし</p>
		}@else{
			<p>キーワードを入力してください</p>
		}
		<ul>
			@for(data of lists(); let i=$index; track i){
				<li>{{ data.name }}</li>
			}
		</ul>
	</div>
	`,
})

export class Lesson3Component{
	ary_data = items
	lists = signal([]);
	word = signal(''); 
	filterData(event){
		let search = event.target.value
		search = this.hiraToKata(search); //メソッドを呼び出し
		if(search != ""){
			this.lists.set(
				this.ary_data.filter((elem)=>{
					return elem.name.indexOf(search) !== -1
				})
			);
		}
		this.word.set(search);
	}
	
    //検索のひらがなをカタカナに直す。
	hiraToKata(word: string){
		return word.replace(/[\u3041-\u3096]/g, (match)=>{
			const chr = match.charCodeAt(0) + 0x60;
			return String.fromCharCode(chr);
		})
	}
}

◆signal

Angular16から登場したリアクティブ操作のためのコンパイラで、変数に対しsignalを設定することで変数がリアクティブとなり、任意のタイミングで更新処理ができます。

値の更新はsetメソッドで行います。また、値の展開はコールバック関数で行うので、hoge()となる点に注意しましょう。

word = signal(初期値); //signalによる制御
this.word.set(更新後の値); //
<!-- 値はコールバック関数となっている -->
<p>{{ word() }}</p>

signalで値を更新する際には、setのほかにupdateというものもあり、これは元の値を再計算する場合に用います。

◆イベントバインディング

ここで文字検索のイベント部分が(input)となっていますが、(hoge)は前述したようにビューからデータに偏方向でデータを受け渡すので、Angularにおけるイベントトリガーはこのように記述するだけで作動します。これをAngularではイベントバインディングと呼んでおり、引数を渡すこともできます($event)とすれば、引数.target.プロパティとしてタグ上のプロパティ(今回はe.target.value)を取得できる。

◆分岐について

Angularは@ifコントロールフローが実装され、それと並行して@else ifと@elseも実装されました。これはelse構文が存在しなかったng-templateより、格段に記述しやすく、見やすくなっています。

sample3-angular.component.html
		@if(lists().length > 0){
			<p>検索結果{{lists().length}}件</p>
		}@else if(lists().length == 0 && word() != ''){
			<p>検索結果なし</p>
		}@else{
			<p>キーワードを入力してください</p>
		}

◆型の定義

AngularはTypeScriptを使用しています。そのため、型の指定に厳格なので、型も事前に定義しておく必要があります。

search.ts
export type Item={
	id: number,
	name: string,
	img: string,
}
export interface Items{
	items:Item[]
}

※型については続編で触れています。

演習4: データリストの追加、削除、修正(DOMの再生成)

いわゆるCRUD(Create、Read、Update、Delete)と呼ばれる操作です。これについても各フレームワークの癖も掴みながら解説していきたいと思います。

※また、データ修正においては本来、修正ページへ遷移、データの更新という二段階の作業を行いますが、今回はあくまで演習なので単一コンポーネントで記述していきます。

Vue

Vue.jsを用いて新規作成と削除を実装するとこのようになります。Reactとの違いは、元のオブジェクトの値も操作できる点があります。ただ、それだと元のオブジェクトの値を破壊してしまうので、別の変数に移し替えておくようにしましょう。ただ、それにあたって新たな技術、ライフサイクルフックが必要となります。

vue3-lesson4.html
<script setup>
import {ref,reactive,onMounted} from 'vue';
    let ins_item = ref('');
    let datalist = reactive([])
    const ary_data =[
        {id: 1,name: "モスクワ",img:"Moscow.jpg"},
        {id: 2,name: "サンクトペテルブルク",img:"Sankt.jpg"},
        {id: 3,name: "エカテリンブルク",img:"Yekaterin.jpg"},
        {id: 4,name: "ムンバイ",img:"Mumbai.jpg"},
        {id: 5,name: "ベンガルール",img:"Bengaluru.jpg"},
        {id: 6,name: "コルカタ",img:"Kolkata.jpg"},
        {id: 7,name: "サンパウロ",img:"SaoPaulo.jpg"},
        {id: 8,name: "リオデジャネイロ",img:"Rio.jpg"},
        {id: 9,name: "ブラジリア",img:"Brasilia.jpg"},
    ]
    //ライフサイクルフック
    onMounted(()=>{
      Object.assign(datalist,ary_data) //空オブジェクトで分割代入する方法
    })
    //登録
	const ins = ()=>{
		let key = datalist.length;
		let items = {
			"key": key,
			"name": ins_item.value
		}
        datalist[key] = items //データの挿入
	}
	//削除
	const del = (idx)=>{
        datalist.splice(idx,1)
	}
	//修正
    const upd = (idx,name)=>{
      const items = [...datalist]
      const item = {...items[idx]}
      item.name = name
      datalist[idx] = item //修正は分割代入で対応する
    }
</script>

<template>
	<input type="text" v-model="ins_item">
			<button type="button" @click="ins">新規</button>
	<ul>
		<li v-for="(data,idx) in datalist">
			<dl>
				<dt><input type="text" v-model="data.name"></dt>
				<dd ><button type="button" @click="upd(idx,data.name)">修正</button></dd>
				<dd ><button type="button" @click="del(idx)">削除</button></dd>
			</dl>
		</li>
	</ul>
</template>

■ライフサイクルフック

Vueにはライフサイクルフックというものが用意されており、一定のタイミングで処理を実行できます。今回は最もスタンダードなonMountedを使用しています。これは、テンプレート読込後に、一度だけ処理を実行してくれます。

   //ライフサイクルフック
    onMounted(()=>{
        //テンプレートレンダリング後の処理
    })

ライフサイクルフックはほかにonBeforeMountというものもあり、これはテンプレート読込前に処理を行います(その他数種類のライフサイクルフックが用意されています)。

※Vue2以前に存在したcreateフックはset upに集約されているので廃止されています。

※v-forを用いる場合の注意点

一瞬、v-ifとv-elseの各ディレクティブを用いて値を制御したくなるでしょうが、それはやってはいけません。マニュアルにはv-forディレクティブの中にv-ifやv-elseを用いると想定外の動作が起きることを警告しています。

v-for と一緒に v-if を使うのを避ける

reactiveで参照渡しする

reactiveは以下のように空オブジェクトを代入しておく方法もあります。refはv-modelディレクティブを直に参照できるので便利なのですが、値を展開する際にはvalueプロパティを付与しないといけません。対してreactiveは以下のように空オブジェクトを代入しておくだけで参照渡しになるので、変数を同期させることができます。

しかし、そのままでは分割代入ができない(つまりは値の更新)ので、解決の方法を探っているとライフサイクルフック上でObject.assignを使用する方法があるみたいです。

React

ReactでもVue.jsと同様にオブジェクトを操作してみます。

lesson.jsx
import { useState,useEffect } from "react"; //ローカルの場合で、useStateを使用するための定義
function Lesson(){
    //変数定義
		const [insitem, setInsitem] = useState('');
		const [upditem, setUpditem] = useState('');
    const [bind ,setBind] = useState([]); //フックの使用(データリストの更新)
		const	ary_data = [
        {id: 1,name: "モスクワ"},
        {id: 2,name: "サンクトペテルブルク"},
        {id: 3,name: "エカテリンブルク"},
        {id: 4,name: "ムンバイ"},
        {id: 5,name: "ベンガルール"},
        {id: 6,name: "コルカタ"},
        {id: 7,name: "サンパウロ"},
        {id: 8,name: "リオデジャネイロ"},
        {id: 9,name: "ブラジリア"},
		];
		const ins = ()=>{
			setBind([
				...bind,
			{
				id:  bind.length + 1,
				name: insitem
			}
			])
		}
		const del = (idx,name)=>{
			setBind(bind.filter((_, i) => i !== idx))
		}
		const upd = (idx,name)=>{
			changeValue(idx,name)
		}
		const changeValue = (idx,item)=>{
			let edit = []
			bind.map((elem,i)=>{
				if(i !== idx){
					edit = [...edit,elem]
				}else{
					edit =  [...edit,{id:i + 1, name: item}]
				}
			})
			setBind(edit)
		}
    //ループ処理の分離
    const taglist = bind.map((data,idx)=>{
				return(
					<li key={idx}>
							<dl>
									<dt><input type="text" onChange={ (e)=>{ changeValue(idx,e.target.value) } } value={ data.name } /></dt>
									<dd><button type="button" onClick={ ()=> upd(idx,data.name) }>修正</button></dd>
									<dd><button type="button" onClick={ ()=> del(idx) }>削除</button></dd>
							</dl>
					</li>
				)
    })
		
		
    //useEffectフックでコンストラクタ的な働きを行う
    useEffect( ()=>{
				setBind(...bind,ary_data)
    },[]);
    //レンダリング
    return(
        <>
			<label>新規<input type="text" onChange={ (e)=>{ setInsitem(e.target.value) } } value={insitem}/></label>
			<button type="button" onClick={ ins }>新規</button>
			<ul>
			{ taglist }
			</ul>
        </>
    );
}
export default Lesson

●ロード時の制御

意外と引っかかりがちなのが、読込直後の制御で、いわばオブジェクト指向のコンストラクタっぽいことをしたい場合にどう記述するがですか、これもuseEffectフックを利用します。初期値に空オブジェクトを入れておけば、読込直後にフックが働き、setBind変数の働きによって、空の変数bindにary_dataのオブジェクトが代入されることになります。

●登録

登録機能において厄介な点は、新規挿入する値はinputタグのテキストの値なのに対し、メソッドの実行はbuttonタグのonCickイベント内の関数insになります。したがって、リアクティブにテキスト操作は行いません。だとするとonChangeイベントの付記は不要かと思われますが、それをするとReactのルール違反になってしまい、警告が出ます。なので、テキストを直接操作しない場合でも、フックを実装しておきましょう(readOnlyと記述する方法もありますが、そうするとテキストへの入力ができなくなってしまいます)。

メソッドinsの中身についてですが、これはロード時の制御と同じ理屈で、リアクティブに制御しているオブジェクトbindに対し、随時分割代入で、オブジェクトを追加しているだけです。そして、テキストの値はvalueプロパティでリアクティブ制御しているので、直接取得することができます。

●削除

この削除における制御も色々と注意点があります。

●コンポネントの内側で関数を呼び出す場合

削除ボタンに実装しているonClickイベントはインラインメソッドを呼び出しています。こうしないと何度もイベントを呼び出してしまい、React limits the number of renders to prevent an infinite loopというエラーが発生してしまいます。なので、コンポネントの内側のイベントでメソッドを呼び出す場合は無名関数(インライン関数)を用いて、メソッドを制御しておきましょう。

●削除の仕組み

削除用のメソッドはdelですが、この中ではarray.filter関数(任意のオブジェクトで引数と一致するものだけを抽出するメソッド)を用いています。そして

bind.filter((_,i) => i !== idx ))

とだけ記述されていますが、これは

bind.filter(function(item,i){ i !== idx }))

と同様で、i !== idx に当てはまる(つまり、削除しようとしたインデックスidxとループしているインデックス番号iが一致している)もの以外をデータとして残す動きをしています。そして、ここでもVue.jsと同様に間違ってもインデックス番号を元に削除操作をしないようにしてください。そんなことをすると値がどんどんずれて、誤操作を招きます。

●修正

表示データをテキストボックスに変更し修正できるようにしてみました。ところが、リアクティブに変更させるということは、データの値を書き換えた時点でリストを再生成することになるので、結局更新ボタンの有無に関係なく処理が行われることになります(手前の値を削除すれば更新されているかはわかります)。

注意点はオブジェクトの値を入れ替えるときで、次のような分割代入を用いれば、対象箇所だけの修正を簡潔に処理できます。

bind.map((elem,i)=>{
    const edit = [...bind] //スプレッド構文によって(更新対象でないオブジェクト上の値を補完)
    if(i === idx){
       elem.name = item
       edit[i] = elem //対象の値を更新する
       setBind(edit) //更新対象の値を同期する
    }
})

分割代入について押さえておきたい大事なポイントは...hogeという表記ですが、これはスプレッド構文と呼び、更新対象でないオブジェクト内のデータを補完する役割を持ちます。具体的には、更新したい情報はelem.nameだけですが、更新情報にいちいち他のプロパティも書いていられません。そこで、このスプレッド構文を分割代入に用いることで、他の更新対象でない値も一度に同期を取りたいオブジェクトに格納してくれるわけです。

Angular

同じくAngularで制御してみます。ところがAngularの場合は、今回のケースに限り、どれよりも簡単です。

lesson.component.ts
import { Component,signal } from '@angular/core';
import {FormsModule } from '@angular/forms';
export type Item={
	id: number,
	name: string,
}
export interface Items{
	items?:Item[]
}
let items = [
	{id: 1,name: "モスクワ"},
	{id: 2,name: "サンクトペテルブルク"},
	{id: 3,name: "エカテリンブルク"},
	{id: 4,name: "ムンバイ"},
	{id: 5,name: "ベンガルール"},
	{id: 6,name: "コルカタ"},
	{id: 7,name: "サンパウロ"},
	{id: 8,name: "リオデジャネイロ"},
	{id: 9,name: "ブラジリア"},
];
@Component({
  selector: 'app-lesson',
  imports:[FormsModule],
  template: `
	<div id="app">
			<input type="text" [(ngModel)]="ins_name" />
			<button type="button" (click)="ins(ins_name)" >新規</button>
			<ul>
				@for(data of items; let i = $index ; track i ){
					<li id="li">
						<input type="text" [(ngModel)]="data.name">
						<button type="button" (click)="upd(i,data.name)">修正</button>
						<button type="button" (click)="del(i)">削除</button>
					</li>
				}
			</ul>
	</div>
	`,
})

export class LessonComponent{
	ins_name = ""
	items = items
        //新規
	ins(ins_name){
		if(ins_name != ""){
			const idx = Number(this.items.length + 1);
			this.items.push({"id":idx,"name":ins_name});
		}
	}
	//修正
	upd(idx: number,upd_item: string){
		this.items.splice(idx,1,{id:idx,name:upd_item});
	}
	//削除
	del(idx){
		this.items.splice(idx,1);
	}
}

Angularの場合は、これで動きます。今回はリアクティブ制御の必要性が薄いのでダイレクトにオブジェクトを代入していますが、Angularの場合は変数が制御クラスの外側に置かれているのと、バインディングでイベントやプロパティを制御するためオブジェクトにproxy制御がかからないので、これで問題ありません。

これはjQueryなどによる、ワンクッションを置いた処理(必ずしもリアルタイムである必然性がない)であるからで、Angularの場合はオブジェクトをReactiveに制御するというよりは、Ajaxでやりとりしているようなものだからです。

※signalはあくまでプリミティブなデータの同期やそれに関連する操作に対し、処理を施すものです。

総括

今まではあくまで基礎なので、単一コンポーネントで演習してきましたが、続編からは複数のコンポーネントを用いた処理がメインとなります(親子コンポーネント処理、ルーティング処理、スタイル処理、TypeScript)。容量が圧迫したために続編を作成していますので、ぜひ宜しくお願いいたします。

ただ、ここまで履修された方なら、たぶんソースの読解力、理解力も格段に上がり、サーバサイドでの開発も抵抗なく付いていけると思いますので、ご健闘お祈りします。また、昨今ではVueもReactもTypeScriptを用いた記述が主流になってきている部分もありますが、基本はやはりJavaScriptでの記述をこなしてからです。

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

なお、自分はこのように知らない言語は、知っている言語と知らないもの同士最低2セットを並行しながら学習していき、共通点と相違点を洗い出していくということをよくします。ですので、今回は当初の記述にあったjQueryに合わせ、VueとReactを敢えて並行して学習し、そして2つに慣れたところでAngularも加えて演習していくという手段をとっており、今はSvelte、そしてsolidJSでも同じような実践を行っております。

慣れてきたら、この5種類のフレームワークで全く同じ働きをするプログラムを作ってみるのも面白いと思いますし、更に理解力が高まると思います。

588
646
7

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
588
646

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?