20190622のReactに関する記事は2件です。

Reactの基本まとめ(Hooks含む)

最終的にReactでTodoリストを構築します。
Reduxなどの状態管理ライブラリは含みません。

Reactとは

Reactは、クライアントサイドでWebページのレンダリングを行うサイトSPA(Single Page Application)を構築する際に便利なライブラリーです。
SPAでは、Webページ表示後も引き続きクライアント側でコンテンツの表示非表示等の状態管理を、継続的に行う必要があります。
Reactでは、予めJSによるロジックとHTMLがセットになった部品(コンポーネント)を用意しておき、それらを組み合わせることでWebページを構成する、コンポーネントベースと呼ばれる考え方を採用しています。
Reactは内部でJSのロジックと対応するDOM要素をマッピングしており、JS側の処理に応じて自動でDOM要素を更新してくれます。
このあたりの仕組みは仮想DOMと呼ばれています。

静的ページやサーバーサイドレンダリングのページでは、引き続きjQueryも便利かなと思っています。

Reactの本家サイトは、最近日本語にも対応しておりとても充実しています。
ここのチュートリアルを一通り実践することもお勧めいたします。

Node.jsとnpmの利用環境が既に整っていることを前提としています。
Node.js / npmをインストールする(for Windows)
過去に投稿したこちらの記事でも触れています。
Node.jsとExpressでローカルサーバーを構築する(1) ―Node.jsとnpmの導入―

環境構築

React公式では、Create React Appというスターターキットを用意しています。
今回は、簡単にはじめられるParcelを利用して環境を構築したいと思います。

インストール

React本体と、ReactとDOM要素をつなげるためのReactDOMをインストールします。

$ npm install --save react react-dom

引き続き、ParcelとBabelのReact用プリセットをインストールします。

$ npm install --save-dev parcel-bundler @babel/preset-react

トランスパイラ

Reactは、通常のJavaScript構文だけで構築することも可能ですが、基本的にJSXと呼ばれる構文を利用するのが一般的です。
ブラウザに導入されているわけではないので、トランスパイラと呼ばれるツールを用いてJavaScripの構文に変換(トランスパイル)する必要があります。
このトランスパイルには一般的にBabelというツールが用いられます。

公式提供スターターキットのCreateReactAppでも内部でBabelが利用されているようです。
Babel自体は、自分で色々設定が出来る自由度の高いツールであると同時に、設定が面倒なツールでもあります。
ParcelはこのBabelを内包した上で、細かな設定をせずともすぐに使えるように作られています。
プリセットは、Babelのオプション用パッケージのことを指します。
ReactのJSXのトランスパイルはオプション機能として提供されている為、別途インストールが必要です。
Parcelの場合、インストールするだけで設定は特に不要です。

バンドラ

es2015にて、JavaScripにモジュール機能(ESModules)が導入されました。
用途ごとに分割された外部JSファイルを、importexport文を通して利用する仕組みです。
HTMLファイル上での読み込み順序を気にすることなく、JS側でファイルの依存関係の管理が完結します。
また、各モジュール毎にスコープが閉じているので、ファイル間での名前の衝突の心配がなくなります。

既にブラウザに機能導入が進んでいるようです。
対象のJSファイルを読み込む際に<script type="module">とすると、ESModulesの対象となります。
ただ、読み込むファイル数が増えればリクエストの回数も増えてしまうので、今のところモジュールバンドラーというツールを用いて、ある程度まとめてしまう(バンドルする)のが一般的です。

モジュールバンドラーは色々ありますが、特に人気なのはwebpackでしょうか。
webpackもBabelと同様、自由度が高い分、設定の手間が多いツールです。
実は、Parcelはバンドルもしてくれます。
こちらも細かな設定は不要なので、インストールした時点でほぼほぼ環境構築が完了しています。

JavaScript modules | MDN
webpackとBabelについては、過去に投稿した記事もございます。
webpackとBabelの基本を理解する(1) ―webpack編―

Reactコンポーネントの基本

JSXをHTMLで出力する

ひとまず、Reactを使って何かしらのHTMLを出力してみましょう。
下記のフォルダ構成でファイルを用意します。
ファイルの拡張子はjs以外にjsxも使えます。

root
  └ src
      ├ index.html
      └ index.jsx
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width,initial-scale=1">
  <title>React Todo</title>
</head>
<body>
  <div id="root"></div>
  <script src="index.jsx"></script>
</body>
</html>
// Reactパッケージの読み込み
import React from 'react';
import ReactDOM from 'react-dom';

// Reactコンポーネント
class App extends React.Component {
  render() {
    return (
      <h1>Hello React!</h1>
    );
  }
}

// HTMLタグにReactコンポーネントを紐付ける
ReactDOM.render(
  <App />,
  document.getElementById('root')
);

rootフォルダで下記コマンドを実行してみます。

npx parcel src/index.html

Parcelによるバンドル

初回は./distフォルダが作成され、html,js,mapファイルが出力されています。
コマンドで指定したhtmlをスタート地点として、<script>で読み込んでいるJSファイルからimport文を頼りに芋づる式に辿って、一つのファイルにまとめていきます。
出力されたJSファイルには暫定的な名前がつけられ、HTMLファイルの方もこれを参照するように書き換わっています。
mapファイルは、元ファイルと出力後ファイルのコードの対応を示す情報です。
ブラウザのDevToolなどでデバッグする際に、元ファイルの方を参照することが出来ます。

実は、Parcelはテストサーバも立ち上げてくれます。
コンソールに出力されたローカルサーバのURLにアクセスすると、出力を確認できます。
また、特に指定をしなければwatchモードで起動する為、関連ファイルを編集すると勝手にコンパイル(トランスパイルとバンドル)を実行してくれます。

reactTodo1.GIF

カスタムコンポーネント

React.Componentクラスを継承したサブクラスAppが、ユーザ定義のReactコンポーネント(カスタムコンポーネント)です。
名前は大文字で始める必要があります。
Appクラスのrenderというメソッドにて、戻り値を定義している部分に書かれているHTMLのようなものがJSXです。
正確にはXMLに近く、空要素には必ずスラッシュ/を入れる必要があります。

React.Component | React

ReactDOM

ReactコンポーネントとDOM要素をつなぐ役割を果たします。
上記の例では、静的メソッドrenderを利用して、<div id="root">配下にAPPで定義したHTMLが展開されるようにしています。

ReactDOM | React

対話による状態変更

ボタンのクリックに応じて、文字が切り替わるようにしてみます。

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      isMorning: true
    };
  }
  render() {
    return (
      <div>
        <button
          onClick={e => {
            this.setState(
              { isMorning: !this.state.isMorning }
            );
          }}
        >
          Click
        </button>
        <h1>
          {this.state.isMorning ? 'Good Morning' : 'Hello'} React!
        </h1>
      </div>
    );
  }
}

buttonタグをクリックする度に文字が切り替わるようになりました。
このコンポーネントは、内部で状態isMorningを保持・管理しています。
クリックするとisMorningの状態が切り替わり、それに応じてHTMLも再レンダリングされます。

reactTodo2.GIF

renderメソッド内のJSXの部分が、通常のHTMLとは様子が異なってきました。
onClickはHTMLタグのonclick属性とは異なる、JSX側の構文です。
用途はonclick属性と同様、HTML要素にイベントハンドラを設置する為のものです。
h1のテキストコンテンツ部分には、JSの三項演算子が追加されました。
isMorningの値に応じて異なる文字列を返すようにしています。

つまりHTMLにJSのコードを埋め込んだものがJSXです。
埋め込み部分は波括弧{ }で囲みます。
PHPなどでテンプレートページを作る時と要領は似ており、JSの実行結果がHTML上に描画されます。
コンテンツのデータ、そのデータの処理ロジック、描画が一通りセットになったオブジェクトがReactコンポーネントです。
このコンポーネントを適宜組み合わせることで、状況に応じたWebページを表現します。

JSXの導入 | React

renderメソッド

サブクラスで必ず定義しなくてはならないのが、renderメソッドです。
renderメソッドでは、コンテンツをどう描画するのかをJSXなどで定義した情報を返す必要があります。
コンテンツに変更があれば、Reactはrenderメソッドを呼び出し、定義に基づいてコンテンツに紐づくHTML要素を更新します。
この際、全体を丸ごと更新するのではなく、差分を確認して必要な部分のみ更新してくれます。

差分の確認およびDOM要素の更新は、ReactDOMが担っています。

render() | React

stateとsetState

前項で「コンテンツに変更があれば」と表しましたが、renderメソッドにの実行に関わる情報は、厳密にはコンポーネントの「状態」を表す情報です。
このコンポーネントの「状態」を表す情報は、クラスのstateプロパティとして、コンストラクタ内で定義します。
そして、このstateプロパティを更新する専用メソッドsetStateを利用して、コンポーネントの状態を更新します。
するとrenderメソッドが呼び出され、stateに紐づくHTML要素が再レンダリングされます。
プロパティをstate以外の名前で定義したり、直接更新するなどのsetStateメソッド以外の手段でstateを更新した場合、renderは呼ばれません。

上記サンプルコードにてボタンがクリックされた時に行っていることは、状態の変更だけです。
this.state.isMorningの値を変更しているだけです。
isMorningの値に紐づいているh1タグは、Reactが勝手に更新してくれます。
HTMLのDOM要素を直接管理する手間から開放され、JavaScript上でのデータ管理を気にかけるだけで済みます。

setStateを利用したstateの更新例

setStateで渡されたstateの断片は、最終的にthis.stateにマージされます。
配列を利用する場合は一旦複製する必要があります。

// stateを複数定義する
this.state = {
  stateA: 'hoge',
  stateB: [ 'cat', 'dog' ]
};

// 特定のstateを更新する
this.setState({
  stateA: 'moimoi'
});

// 配列の場合は、一旦複製
const stateB = this.state.stateB.slice();
stateB.push('rabbit');
this.setState({ stateB });

// 下記では再描画が発生しない
this.state.stateB.push('rabbit');
this.setState({ 
  stateB: this.state.stateB
});

Reactは、更新内容を最小限にとどめる為に差分チェックをします。
setStateを実行しても、前回と値に変わりが無ければ再レンダリングされません。
その際はObject.isのアルゴリズムに基づいて比較を行います。
配列は、保存されたメモリの位置でざっくりと比較されるので、新規配列を渡す必要があります。

const stateA = ['cat', 'dog'];

const stateB = stateA;  //stateAもstateBも同じ配列を参照している
const stateC = stateA.slice();

stateB.push('rabbit');
stateC.push('rabbit');

console.log(Object.is(stateA, stateB)); // true
console.log(Object.is(stateA, stateC)); // false

setState() | React
state とライフサイクル | React

イベント処理

イベントハンドラに無名関数を渡すのではなく、クラスのメソッドを設定する場合は以下のようになります。
Reactに限った話ではありませんが、イベントリスナーを登録する際にelement.addEventListener('click', this.someMethod)の要領でメソッドをそのまま渡しても、コールバックで実行された関数内でのthisの参照はそのメソッドが属するオブジェクトにはなりません。
素のJSでは、イベントを登録したDOM要素がthisになります。Reactの場合はundefinedでした。
その為、コンストラクタ内にてbindメソッドで所定のオブジェクトをthisとして参照する関数を生成して、上書きしています。

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      isMorning: true
    };
    // コールバック関数内で、thisの参照がこのクラスを指すための設定
    this.handleClick = this.handleClick.bind(this);
  }
  // クリック時のハンドラ
  handleClick(e) {
    this.setState(
      { isMorning: !this.state.isMorning }
    );
  }
  render() {
    return (
      <div>
        <button onClick={this.handleClick} >
          Click
        </button>
        <h1>
          {this.state.isMorning ? 'Good Morning' : 'Hello'} React!
        </h1>
      </div>
    );
  }
}

無名関数を直接渡す場合は、アロー関数を利用します。

イベント処理 | React

propsによるデータの受け渡し

複数のコンポーネントを組み合わせてこそのReactなので、ボタンを別のコンポーネントとして切り出してみます。

// 子コンポーネント
// ボタンコンポーネント
class MyButton extends React.Component {
  // state等の宣言をしない場合は省略可
  constructor(props) {
    super(props);
  }
  render() {
    return (
      <button onClick={this.props.onClick} >
        {this.props.text}
      </button>
    );
  }
}

// 親コンポーネント
class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      isMorning: true
    };
    this.handleClick = this.handleClick.bind(this);
  }
  handleClick(e) {
    this.setState(
      { isMorning: !this.state.isMorning }
    );
  }
  // <button>の代わりに<MyButton />
  render() {
    return (
      <div>
        {/* 文字列とイベントハンドラを渡す */} 
        <MyButton text="Click" onClick={this.handleClick} />
        <h1>
          {this.state.isMorning ? 'Good Morning' : 'Hello'} React!
        </h1>
      </div>
    );
  }
}

JSX上で、一つのタグとしてコンポーネント名を記述します。
この際、子コンポーネントとなるコンポーネントに値を渡すことが出来ます。
受け取った側のコンポーネントは、this.propsからこの値にアクセスできます。
コンストラクタ内で受け取っているpropsがそうです。
this.propsは読み取り専用です。this.stateの様に更新することは出来ません。
this.propsもまた、更新されるとrenderメソッドが呼び出され、紐づくDOM要素が再レンダリングされます。

外部から渡される読み取り専用データpropsと、内部で管理する制御データstate、この二つがコンポーネントのレンダリング実行に関係します。

尚、Reactコンポーネントが大文字で始める必要があるのは、通常のHTMLタグと区別する為です。
<div>はhtmlのdivタグとして認識されますが、<Div>はReactコンポーネントと見なされます。
スコープ内に該当コンポーネントが無い場合はエラーになります。

関数コンポーネント

上記ボタンコンポーネントは、内部で状態stateを持ちません。
受け取ったpropsをJSXで展開するのみです。
この場合、クラス構文ではなく関数の構文で定義する方が好ましいです。

// 一つの連想配列でpropsを受け取る
function MyButton(props) {
  return (
    <button onClick={props.onClick} >
      {props.text}
    </button>
  );
}

// 分割代入が便利
function MyButton({ onClick, text }) {
  return (
    <button onClick={onClick} >
      {text}
    </button>
  );
}

renderメソッドと同じく、JSXを返します。
Reactがインポートされているモジュール内であれば、そのままReactコンポーネントとして認識されます。

Hooks

ReactのVer.16.8から、新機能Hooksが導入されました。
Hooksは、関数コンポーネントに追加できる様々な機能群です。
クラスコンポーネントでしか出来なかったことが、関数コンポーネントでも出来るようになります。
公式によると、クラスコンポーネントはトランスパイル後のソースにて、関数コンポーネントほど最適化されていないようです。
今のところクラスコンポーネントを将来的に廃止するとかそういう話にはなっていませんが、これからはシンプルな関数コンポーネントの組み合わせによる構成が推奨されているようです。

フックの導入 | React

ステートフック

ステートフックは関数コンポーネントにも内部状態を持たせる仕組みです。
これまで、stateを保持する場合はクラスコンポーネント、保持しない場合は関数コンポーネントと使い分けていたのが、Hooksを利用すると関数コンポーネントでもstateを持つことが出来ます。

上記のクラスコンポーネントAppをHookを利用した関数コンポーネントに書き換えてみます。

import React, { useState } from 'react';

function App() {
  // useStateの引数はstateの初期値
  const [isMorning, toggleFlag] = useState(false);

  return (
    <div>
      <MyButton
        onClick={e => toggleFlag(!isMorning)}
        text="Click"
      />
      <h1>
        {isMorning ? 'Good Morning' : 'Hello'} React!
      </h1>
    </div>

  );
}

useState関数をインポートして利用します。
必ず、関数コンポーネント直下のスコープで実行する必要があります。
引数はstateの初期値です。
戻り値は配列です。0番目が現在のstate、1番目がstateを更新する為の関数です。分割代入で受け取っています。
それぞれ、クラスコンポーネントのthis.statethis.setStateと同じ役割を果たします。
初回実行時はuseStateの引数で渡した初期値をstateとして受け取ります。
2回目以降は現在のstateの値(更新用関数で更新した場合は更新後の値)を受け取ります。
必要に応じて複数のstateを用意することも出来ますし、クラスコンポーネントの様に一つのオブジェクトで管理する事もできます。

function Example() {
  // 別々にstateを用意する
  const [count, setCount] = useState(0);
  const [animal, setAnimal] = useState(['cat']);

  // もしくは一つのオブジェクトで定義
  // 但し、this.setStateとは異なり、古いものにマージされるのではなく置換されるので注意
  const [state, setState] = useState({
    count: 0,
    animal: ['cat']
  });

ステートフックの利用法 | React

自己定義関数
useState関数はレンダリングの度に実行されますが、引数が有効なのは初回レンダリング時のみです。
実際に中のソースがどうなっているのかは分かりませんが、挙動としては自己定義関数が近いでしょうか。

let animal = (first) => {
  // 初回しか実行されない処理
  console.log(first);
  // 自らを上書き
  animal = () => {
    // 2回目以降に実行される処理
    console.log('cat!');
  };
};

animal('dog!'); // dog!
animal('dog!'); // cat!
animal('dog!'); // cat!

Todoアプリ

シンプルなTodoアプリを作成します。

ReactTodo.gif

Todoリストを表示する

ひとまず、予め用意したTodoリストを表示するコンポーネントを作成します。
リストは配列で管理します。

import React, { useState } from 'react';
import ReactDOM from 'react-dom';

// Todo項目
function Todo({ text }) {
  const [completed, setState] = useState(false);

  return (
    <li
      onClick={e => setState(!completed)}
      style={{
        textDecoration: completed ? 'line-through' : 'none'
      }}
    >
      {text}
    </li>
  );
}

// Todoリスト
function TodoList() {
  const [todos, setTodo] = useState(
    [
      '牛乳 2本',
      '卵 10ヶ入 1パック',
      '食パン 5枚切り 1袋'
    ]
  );

  // todosを基に<Todo />の配列を作成
  return (
    <ul>
      {todos.map((todo, index) => (
        <Todo key={index} text={todo} />
      ))}
    </ul>
  );
}

ReactDOM.render(
  <TodoList />,
  document.getElementById('root')
);

コンポーネントTodoは、一つのtodo項目を表します。
完了の状態を保持し、クリックすると完了を示す打ち消し線が表示され、再度クリックすると未完了の状態になります。
JSXにてstyle属性を適用したい場合は、オブジェクト{style名:値}を渡します。
text-decorationの様なハイフン-を含む名前は、ローワーキャメルケースで指定します。

コンポーネントTodoListは、todoリストを保持します。
配列をJSX内で展開する場合はmapメソッドを利用します。
その際、key値も指定する必要があります。
keyはReactが内部でリストの各要素を識別する為に用いる為、リスト内で一意の値でなくてはなりません。
固有のID等の値が無い場合、配列のインデックス値を適用することも可能ですが、インデックスは随時振りなおされるものなので、あまりお勧めは出来ません。

リストと key | React

Todoを追加する

Todoを自分で入力するためのコンポーネントを追加します。

function Todo({ text }) {
  /* 変更なし */
}

// Todoを追加する
function AddTodo({ addTodo }) {
  const [inputText, setInputText] = useState('');

  return (
    <form
      onSubmit={e => {
        e.preventDefault(); // 素のJSのEvent.preventDefault()と同じ
        if (!inputText.trim()) {
          return;
        }
        addTodo(inputText);
        setInputText('');
      }}
    >
      <input
        type="text"
        value={inputText}
        onChange={e => setInputText(e.target.value)}
      />
      <button type="submit">Add Todo</button>
    </form>
  );
}

// Todoリスト
function TodoList() {
  const [todos, setTodo] = useState([]);

  return (
    <div>
      <AddTodo addTodo={newTodo => setTodo(todos.concat(newTodo))} />
      <ul>
        {todos.map((todo, index) => (
          <Todo key={index} text={todo} />
        ))}
      </ul>
    </div>
  );
}

ReactにてInputフォームを利用するにあたり、Input要素の状態(value属性の値)を管理する為のstateを用意しています。
入力値を受け取りtodosを更新する関数を、propsを通してTodoListからAddTodoに渡しています。

Ref

DOM要素に直接アクセスする方法として、Refがあります。
React.createRef()メソッドを利用してRefを作成します。
そのRefを対象のReact要素のref属性に設定すると、currentプロパティを通してDOM要素に直接アクセスすることが可能になります。

function AddTodo({ addTodo }) {
  let textInput = React.createRef();

  return (
    <form
      onSubmit={e => {
        e.preventDefault();
        const elInput = textInput.current;
        if (!elInput.value.trim()) {
          return;
        }
        addTodo(elInput.value);
        elInput.value = '';
      }}
    >
      <input
        type="text"
        ref={textInput}
      />
      <button type="submit">Add Todo</button>
    </form>
  );
}

HTML要素ではなく、カスタムクラスコンポーネントのref属性にRefを適用した場合、curretプロパティでアクセスできるのは、そのクラスのインスタンスです。
関数コンポーネントはインスタンスが存在しない為、ref属性は利用できません。
あまり多用するとReactを利用するメリットが薄くなるので、基本的にはstateとpropsを通して管理するのが望ましいです。

RefとDOM

Todoに表示フィルタを追加する

Todoリストを「全て・完了・未完了」で切り替えられるよう、フィルター機能を追加します。

// 表示切替用フィルタ
const VisibilityFilters = {
  SHOW_ALL: 'All',
  SHOW_COMPLETED: 'Completed',
  SHOW_ACTIVE: 'Active'
};

function Todo({ text, filter }) {
  const [completed, setState] = useState(false);

  if (
    (filter === VisibilityFilters.SHOW_ACTIVE && completed)
    || (filter === VisibilityFilters.SHOW_COMPLETED && !completed)
  ) {
    // 表示対象外の場合はnullを返す(非表示)
    return null;
  }

  return (
    <li
      onClick={e => setState(!completed)}
      style={{
        textDecoration: completed ? 'line-through' : 'none'
      }}
    >
      {text}
    </li>
  );
}

function AddTodo({ addTodo }) {
  /* 変更なし */
}

// フィルターボタン
function Link({ active, onClick, children }) {
  // 予めオブジェクトで定義して展開するのもあり
  const params = {
    onClick: onClick,
    disabled: active,
    style: { marginLeft: '4px' }
  };

  return (
    <button {...params}>
      {children}
    </button>
  );
}

// 表示の切替
function Footer({ filter, setFilter }) {
  const linkList = Object.values(VisibilityFilters);

  return (
    <div>
      {linkList.map(myFilter => (
        <Link
          key={myFilter}
          active={myFilter === filter}
          onClick={e => setFilter(myFilter)}
        >
          {myFilter}
        </Link>
      ))}
    </div>
  );
}

// Todoリスト
function TodoList() {
  const [todos, setTodo] = useState([]);
  const [filter, setFilter] = useState(VisibilityFilters.SHOW_ALL);

  return (
    <div>
      <AddTodo addTodo={newTodo => setTodo(todos.concat(newTodo))} />
      <ul>
        {todos.map((todo, index) => (
          <Todo
            key={index}
            text={todo}
            filter={filter}
          />
        ))}
      </ul>
      <Footer filter={filter} setFilter={setFilter} />
    </div>
  );
}

TodoListに、現在のフィルターを表す新たなstatefilterを追加しました。
Todoコンポーネント内では、フィルターと項目の完了状態が一致しない場合にnullを返すようにしています。
nullを返すと、そのコンポーネントはレンダリングの対象から外れます。

props.children

Reactコンポーネントは、空要素以外にコンテンツを閉じタグで囲って記述することも可能です。
この場合、子要素はprops.childrenで取得できます。
他のReactコンポーネントを渡すことも可能で、コンポーネントのネストが深くなることを防ぐことが出来ます。

function Child(props) {
  return <p>{props.children}</p>
}

function OtherChild(props) {
  return <span>{props.text}</span>
}

function Parent() {
  return (
    <Child>
      <OtherChild text="HOGE" />
    </Child>
  );
}

//<p>
//  <span>HOGE</span>
//</p>

props.children | React

Flux

上記例では、TodoのテキストのリストはTodoListコンポーネントが、項目ごとの完了未完了の状態はTodoコンポーネントが保持しています。
大きなアプリケーションでは、このstateが各コンポーネントに散らばっていることで、管理が煩雑になってしまうかもしれません。
Reactを提供するFaceBookでは、アプリケーションの状態を一元的に管理する設計手法として、Fluxという考え方を提示しています。

Fluxに関しては、以前に投稿したこちらの記事がございます。

デベロッパーツール

公式からデバッグ用ツールが提供されています。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React + TypeScript + Webpack を既存プロジェクト(ASP.NET MVC5)に導入してみた

はじめに

 「JQueryもいいけれど、そろそろReactを使ってみたいな」
 そう思って挑戦してみたところ、なかなかに難儀でしたので、それをまとめてみます。「React + TypeScript + Webpack 」をASP.NET MVC5に適用する内容になっています。

 次にような方に役立つのではないでしょうか。

  • JQueryは使ったことあるけれど、Reactを始めてみたい方
  • 1から作るのはサンプルやcreate-react-app をやってはみたものの、既存プロジェクトへの適用の仕方がよく分からない方

各パッケージの役割を整理

 正直なところフロントエンドがここまで複雑化しているのとは思いませんでした。
 たくさん登場人物がいて、さらに誰が何を担当しているのが分かりにくいです…。そもそもなんでフロントエンドの話に、サーバーサイドである、Node.js が出てくんねん!って感じですよ。過渡期なのでしょうけど、ちょっと魔境すぎません?

色々な登場人物が出てくる流れ

 大まかにこういう流れだと理解しています。

1. React を使いたい

  • やることは React.jsをインポートする。JQuery導入と同じ。怖くない。

2. JSXが登場

  • これまでのJavaScriptはHTMLに紐づくものだったけど、WEBサイト機能が複雑になってきたし機能単位でまとめた方がいいよね。JavaScript主体のJSXで書く方が便利。
  • JSXは Babel でコンパイルしないと使えない。Babel 登場。
  • Babelを使うのに Node.jsが必要。Node.jsも登場。

3. TypeScriptが登場

  • JavaScriptを直接書くより、最近のプログラミング言語っぽく書けるTypeScriptを使った方がいいよね。JSXじゃなくてTSXにしよう。
  • TSXの導入に伴い Babel は必要なくなった。でも TSXファイルをコンパイルするのに Node.js が必要なので、こいつは残ります。

4. webpackが登場

  • 機能の拡大と共にJavaScriptファイルが沢山増えてしまった。沢山あると読み込みが遅くなる。そこでwebpackです。こいつは一括で取り込めるようにまとめたJavaScriptを作成できます。ついでに圧縮(miniファイル作成)もできるよ。
  • TypeScriptのコンパイルを実行したり(コンパイルそのものは ts-loaderが行う)、どこにまとめたJavaScriptを置くかも担当します。
  • Webpackを使うのにもやっぱりNode.jsが必要です。

5. Reactを使う

  • 4で出力した JavaScriptを読み込めば使用できます。

 要はブラウザのスクリプト言語としてはJavaScriptがデファクトスタンダード。でもJavaScriptは使いづらいから、それを何とかしようと色々している結果、今現在のような複雑なことになっているのだと思います。

前提

 既存プロジェクトとして、ここでは、ASP.NET MVC5とします。.NET Coreじゃなく.NET Framework 4.7.2 の方にしてみます。(その方が既存プロジェクトっぽいし、.NET Coreの方はテンプレートあるしね……)
 IDEは Visual Studio 2019 Community で確認していますが、Visual Studio 2017でも大丈夫でしょう。

テンプレート通りに作成(既存プロジェクトのつもり)

ASPNet.PNG
MVC5.PNG
開始.png

npmを導入

npmとは

 各種パッケージを管理するものです。Visual Studioのデフォルトとしてある Nugetと同じような機能です。
 同じような機能を二つ使うのはややこしいですが、Nugetに無いので仕方がありません。フロントエンドのパッケージを管理するのが npm、サーバーサイドのパッケージを管理するのが、Nugetと分けるのがよさそうです。
(※デフォルトでは NugetでJQueryやBootstrapを管理しているので、後々削除する必要があります)

拡張機能のインストール

 Visual Studioではデフォルトでは npm を使えないので、拡張機能の管理を呼び出します。
拡張機能開始.png

 ここでは、NPM Task Runner と Package Installerを入れておきます。
NPM関連.png

 WebPack Task Runner もあとで使うので入れておきます。
webpackTaskRunner.png

 一度 Visual Studioを閉じてインストールを実行します。
拡張機能インストール確認png.png

npmでReactをインストール

 ソリューション エクスプローラー で右クリックをして、Quick Install Packagesを選択します。
QuickInstallPackage.png

 npm を選び、react のバージョンを16.8.6(現時点の最新版)を選択し、Installを実行します。同じように react-domもインストールします。
npmInstall.png

 すると、プロジェクトの直下に packeage.json が作られているのが確認できます。これがパッケージを管理するファイルになります。プロジェクト管理にはなっていませんが、package-lock.jsonファイルと node_modulesフォルダも作成されていることが確認できます。node_modulesにパッケージの実体が存在します。

NPMInstall後.PNG
(参考)
package-lock.jsonについて知りたくても聞けなかったこと

package.jsonの変更

 このまま必要なパッケージをnpmコマンドでインストールしていってもいいのですが、直接package.jsonを書き換えた方が早いのでそうします。

packagejson書き換え.PNG

修正後のpackage.json
{
  "name": "myproject",
  "version": "1.0.0",
  "scripts": {
    "build": "webpack",
    "watch": "webpack -w"
  },
  "devDependencies": {
    "ts-loader": "^5.4.3",
    "typescript": "^3.4.4",
    "webpack": "^4.30.0",
    "webpack-cli": "^3.3.1"
  },
  "dependencies": {
    "@types/react": "^16.8.14",
    "@types/react-dom": "^16.8.4",
    "react": "^16.8.6",
    "react-dom": "^16.8.6"
  },
  "private": true
}

 package.jsonの中身の詳細については下記ページが勉強になります。

変更後のpackage.jsonのパッケージをインストール

 Visual Studio のメニューの 表示 -> その他のウィンドウ -> タスクランナー エクスプローラーを表示します。
taskRunner.png

 Defaultsでインストールを実行します。
taskRunner2.png
 インストール終了。
taskRunner3.png

 インストールが成功すると、node_modulesフォルダ内のパッケージが増えていることが確認できます。
npmのinstall後のnodemodule.PNG

(注釈)ここまで「あれ?」と思った方もいると思います。package.jsonを作成して、直接書き換えることが出来るならば「Package Installer」は必要なさそうと思うかもしれません。確かにそうなのですが、今後追加でインストールしたいものが出来た時に便利です。

TypeScriptの設定

 新しい項目の追加で、tsconfig.json を追加します。
typescriptの設定ファイル.png

 そして、tsconfig.json を次のように変更します。

{
  "compilerOptions": {
    "sourceMap": true,
    "target": "es5",
    "module": "es2015",

    "jsx": "react",
    "moduleResolution": "node",
    "allowSyntheticDefaultImports": true,
    "lib": [
      "es2019",
      "dom"
    ]
  }
}

 tsconfig.json の中身の詳細については package.json と同じく下記ページを参考にしています。

webpackの設定

 新しい項目の追加で、webpack.config.js を追加します。

webpackの設定ファイル.png

 そして、webpack.config.js を次のように変更します。
 ここでは「entry」に設定されている "./src/main.tsx"のファイルを ./dist/main.js として変換すると設定しています。

module.exports = {
    // モード値を production に設定すると最適化された状態で、
    // development に設定するとソースマップ有効でJSファイルが出力される
    mode: "development",

    entry: "./src/main.tsx",

    // ファイルの出力設定
    output: {
        //  出力ファイルのディレクトリ名
        path: `${__dirname}/dist`,
        // 出力ファイル名
        filename: "main.js"
    },

    module: {
        rules: [
            {
                // 拡張子 .ts もしくは .tsx の場合
                test: /\.tsx?$/,
                // TypeScript をコンパイルする
                use: "ts-loader"
            }
        ]
    },
    // import 文で .ts ファイルを解決するため
    resolve: {
        extensions: [".ts", ".tsx", ".js", ".json"]
    }
};

 webpack.config.js の中身についてもこちらを参考に(というかコピー)しています。

TSXファイルの追加と変更

 新しい項目の追加で、TypeScript JSXファイルを追加します。場所は上記 webpack.config.js で指定した./src/main.tsxです。

tsxの作成.png

 内容は create-react-app のHello Worldみたくしてみます。

main.tsx
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';

ReactDOM.render(<App />, document.getElementById('root'));

 同じくTypeScript JSXファイルを./src/App.tsx に追加します。

App.tsx
import React from 'react';

const App: React.FC = () => {
    return (
        <div className="App">
            <header className="App-header">
                <p>
                    Hello, World!!
                </p>
                <a
                    className="App-link"
                    href="https://reactjs.org"
                    target="_blank"
                    rel="noopener noreferrer"
                >
                    Learn React
                </a>
            </header>
        </div>
    );
}

export default App;

WebPack Task Runnerでコンパイル

 WebPack Task Runnerを使って、TypeScriptのコンパイルを実行します。
webpackTaskRunner実行.PNG

実行結果
webpackTaskRunner実行結果.PNG

 コンパイルが無事に実行されると ./dist/main.js が作成されます。
distが出来る.PNG

 以後、TSX(TypeSciprt) が変更されるたびにコンパイルするのは面倒なので、ビルドする前にWebPack Task Runnerでコンパイルが走るようにバインドしておきます。
バインド設定.png

main.jsをHTML側で呼び出す

 main.jsを使うために、適当な index.cshtml を変更します。

index.cshtmlを変更
@{
    ViewBag.Title = "Home Page";
}

<div id="root"></div>
<script src="~/dist/main.js"></script>

ビルドして動作確認

 ビルドして、Hello World! が確認できれば無事に React が使えていることになります。

動いた.PNG

おわりに

 登場人物がたくさん出てきて混乱してしまいますが、一つ一つ誰が何をやっているかを理解していければ大丈夫だと思います。色々やってますけど、最終的にはJavaScriptになるというだけです。
 逆に理解しないまま「とりあえずサンプルあるし~」と進めていくと、上手く行っている時はいいのですが、トラブル発生時にどこに問題が発生しているのか分からないことになります。というか、なった。

参考資料

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む