Multi Vitamin & Mineral

Multi Vitamin & Mineral です。プログラムに関することを書いております。

React with TypeScript さらっと一通り知ってみる

プロジェクトの作成

create-react-app

公式サイトにあるとおりに以下のコマンドで雛形を作ります。

create-react-app.dev

$ npx create-react-app my-app --template typescript
$ cd my-app

eslint の導入

下記の記事の通り eslint と Prettier も入れておきます。この記事では詳細は省いて実施した内容だけ書きます。

multimineral-tech.com

$ npx eslint --init

npx eslint --init では、「To check syntax, find problems, and enforce code style」を選びました。私はチェック方法に Airbnb を選びましたが、任意のものを選べばOKです。 TypeScript の利用は y (=yes) としています。

Prettier の導入

Prettier をインストールします。それと、テスティングフレームワークの Jest で警告がでないようにモジュール追加します。

$ npm install ---save-dev prettier eslint-config-prettier eslint-plugin-prettier
$ npm install --save-dev eslint-plugin-jest

$ npm run xxx のようにして使えるコマンドに $ npm run lint を追加します。この後、ファイル保存時に自動で lint が掛かる設定を入れるので、これは無くても問題はないです。

// package.json
{
  // 中略
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject",
    "lint": "eslint src --ext .ts,.tsx" // <=ADD(eslint の実行)
  },
  // 中略
}

eslint の設定

eslint の設定ファイルを変更します。モジュールを動かすための追加行は // <=ADD と記載してます。警告を変更したいための追加行は // <=ADD(不要な警告抑止) と記載してます。

// .eslintrc.js
module.exports = {
  env: {
    browser: true,
    es6: true,
    'jest/globals': true, // <=ADD
  },
  extends: [
    'plugin:react/recommended',
    'airbnb',
    'plugin:prettier/recommended', // <=ADD
    'prettier/@typescript-eslint', // <=ADD
  ],
  // 中略
  plugins: ['react', '@typescript-eslint', 'prettier', 'jest'],// <=ADD ※「, 'prettier', 'jest'」の部分
  rules: {
    'react/jsx-filename-extension': ['error', { extensions: ['.jsx', '.tsx'] }], // <=ADD(不要な警告抑止)
    'import/extensions': [
      'error',
      { extensions: ['.js', '.jsx', '.json', '.ts', '.tsx'] },
    ], // <=ADD(不要な警告抑止)
    'spaced-comment': ['error', 'always', { markers: ['/ <reference'] }], // <=ADD(不要な警告抑止)
    'prettier/prettier': 'error', // <=ADD
  },
  settings: {
    'import/resolver': {
      node: {
        extensions: ['.js', '.jsx', '.ts', '.tsx'],
      },
    },
  }, // <=ADD(不要な警告抑止)
};

eslint から Prettier を呼び出せるように設定されました。 Prettier の設定は上記ファイルにも書けますが、何が Prettier の設定かわかりにくくなりますので .prettierrc に書くことにしました。 Prettier のデフォルトは文字列リテラルがダブルクォートになります。シングルクォートにしたいのでその設定を入れておきます。

// .prettierrc
{
  "singleQuote": true,
}

VSCode の設定

VSCode の設定です。セーブ時に eslint のフォーマッターを掛けます。eslint は Prettier に関連づいているので、 Prettier の設定も反映されます。

// .vscode/setting.json
{
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": true
},
"eslint.format.enable": true
}

自動生成される src\serviceWorker.ts は警告が出てしまいます。そこで、一行目に警告は出さなくて良いよと追記します。自動生成されるソースですので、今回は極力変更せずに警告抑制だけの対応としました。

// src\serviceWorker.ts
// 1行目に以下を追加
/* eslint-disable no-console, no-use-before-define, no-param-reassign */

これで npm run lint を実行します。改行コードやフォーマットで警告が出るので、これらは対処します。

参考

今回の設定で特に参考になったサイトは以下でした。

ginpen.com qiita.com

以下は eslint の公式サイト。コメントで警告を抑制する件は「Disabling Rules with Inline Comments」の項にあります。

eslint.org

基本の書き方

TSX を記述する

JavaScript に HTML タグ的なものを埋め込めるのが JSX です。TypeScript の場合は TSX と呼ばれ、拡張子も .tsx になります。 ここでは、App.tsx を編集して動きを確認してみることにします。

以下のように書き換えて、 $ npm start で開発サーバを立ち上げ、ブラウザで表示を確認してみます。

// src/App.tsx
import React from 'react';
import './App.css';

type Item = {
  id: number;
  title: string;
};

function App() {
  const items: Item[] = [
    { id: 1, title: 'item1' },
    { id: 2, title: 'item2' },
    { id: 3, title: 'item3' },
  ];
  return (
    <div className="App">
      <ul>
        {items.map((item) => (
          <li key={item.id}>{item.title}</li>
        ))}
      </ul>
    </div>
  );
}

export default App;

上記のソースはオブジェクトの配列をリスト表示する例です。

items というオブジェクトの配列を定数として用意し、 <li> の一覧を出力します。

オブジェクトには type で型を用意します。 type は型や型の組み合わせに別名をつける TypeScript の構文です。今回は id , title という要素を持つ Item という型を作っています。 const items: Item[] = ... は、 Item の配列(= Item[] )を型とする items という定数を宣言しています。

関数の return に、出力するHTMLを記述できます。この中で処理(式、変数、など)を使う場合は {} の中に記述します。 <ul> の中で items を繰り返し <li> に変換して出力するプログラムになっています。

配列.map((要素) => 処理)配列 を一つひとつ処理するプログラムが書けます。 要素 に対する処理を 処理 に記述できます。 items を取り出した結果が item で、それから <li key={item.id}>{item.title}</li> というHTMLタグを作り出しています。

ちょっと特殊な話になりますが、 <li><tr> のような繰り返し出力するタグの場合、Reactから key という属性を記述することを求められます。これは React の仕様で、 key は必ず一意になる値である必要があります。以下の例では、オブジェクトに id を用意し、これを設定しています。

関数コンポーネント

新しいタグを作り、更に別ファイルにしてみます。

タグは関数かクラスで作ることができます。ここでは関数で作る関数コンポーネントの話をします。最初の例にあった App も関数コンポーネントでしたが、次の例は、関数コンポーネントを新規に作り、そしてファイルを分割する方法になります。

// src/components/Child.tsx
import React from 'react';

type Props = {
  title: string; // children 以外はHTMLの属性のように利用できる
  children: React.ReactNode; // children はHTMLを受け取る場合の特殊な要素
};

const Child: React.FC<Props> = (props) => {
  return (
    <div>
      <h1>{props.title}</h1>
      {props.children}
    </div>
  );
};

export default Child;

App とは違い、関数をアロー関数の形式にしました。 function Child () { ... } ではなく const Child = () => { ... } にしています。

TypeScript なので、関数の引数と返却値には型が必要です。関数の返却値には React.FC という型を利用しています。関数コンポーネント(FunctionComponent)のことで、React が TypeScript 用に用意してくれています。 React.FC はジェネリクス( <> の中)で引数の型を指定できる仕様になっています。

引数の型に使うため Props という名前(名称は任意でOK)の type を作成しています。この Props でHTMLの属性のようなモノを作ります。 title はHTMLの属性(のようなモノ)として使えます。 title 以外にも任意で増やすことができます。

children は特殊な値でありキーワードです。この名称は、HTML要素を受け取るルールになります。 <Child title="1">2</Child> と書いたときに、 1title に、 2children にあたります。 children 以外のスペルは使えない特殊なものだと考えてください。

最初なので Child の引数は props とあえて書きました。ですが eslint 的には非推奨のため警告がでます。以下のように訂正しておくと良いです。

const Child: React.FC<Props> = (title, children) => {
  // ... 中略 ...
};

次に作成した Child を利用する App.tsx です。

// src/App.tsx
import React from 'react';
import './App.css';
import Child from './components/Child'; // 別ファイルのコンポーネントを取り込む。拡張子は不要。

function App() {
  return (
    <div className="App">
      <Child title="from App.">
        <a href="https://www.google.com/">link</a>
      </Child>
    </div>
  );
}

export default App;

title<Child> タグの属性として使えるようになります。 children<Child></Child> に囲まれた部分を指します。以下の例では <a href="https://www.google.com/">link</a> にあたります。

関数コンポーネントで状態を扱う(Hooks で State を使う)

React Hook の登場により、関数でも状態が持つことができるようになりました。

useState は2つの値を返却する

React Hook で登場した useState という機能を追加します。 import 文に React の useState を利用する(= import React, { useState } from 'react'; )と追記します。

useState は【状態】をひとつ作る機能です。作る【状態】は1つなのですが、2つの値を返却します。 useState 関数は、【状態】の初期値を引数にとり、「【状態】」と「その【状態】を変更する関数」を配列で返却してくれます。2つ返却されることを最初に頭に入れておくと理解がしやすくなります。

Hooks の実例

ソースでの記述内容を例に説明します。

import React, { useState } from 'react'; // , { useState } を追加しました
import './App.css';

function App() {
  const [count, setCount] = useState(0); // Hook で Stateを作成する。
  const handleIncrement = () => {
    setCount(count + 1); // useState で作った setCount を呼びだす。 count の値を変更できる。
  };

  return (
    <div className="App">
      <p>{count}</p>
      <button type="button" onClick={handleIncrement}>
        increment
      </button>
    </div>
  );
}

export default App;
  • useState(0) : [count, setCount] を返却してくれる。 0 は 状態( = count )の初期値になる。
  • [count, setCount]
    • count : 状態。関数内では定数として利用できる。
    • setCount : 状態( = count )を変更する関数。関数内で呼び出すことができる。
  • setCount(count + 1); : countcount + 1 に変更する。

初期値が 0 のカウンタを作っています。

作られた【状態】であるカウンタは、 <p>{count}</p> で表示されます。

ボタン <button type="button" onClick={handleIncrement}>increment</button> をクリックするとカウントアップする仕組みになっています。 handleIncrement という関数を定数で作っています。 const handleIncrement = () => { ... } の部分です。この中で setCount(count + 1) をして、カウンタの値をインクリメントしています。

なぜ Hooks が必要であるか

さて、なぜこんなまどろっこしいことをしなくてはならないのか。普通に関数内に const count = 0; を宣言して count = count + 1; でカウントアップできないのか。

React は画面描画するタイミングで関数が再実行されます。もし const count = 0; として宣言をしていたら、ボタンを押して再描画するタイミングで再度 const count = 0; が実行されます。つまり永遠に 0 になるわけです。 この問題を解決するには、関数とは別のところで【状態】を持つ必要があります。 方法は色々ありますが、 React Hooks はその解決方法のひとつになります。

Redux で状態を扱う

Hooks の限界

関数コンポーネントで状態を持つことができたのが Hooks でした。 例えば、Hooksで作った setCount のような状態を変更する機能をいろんなコンポーネントで使いたくなったとします。その場合は、 setCountProps の要素として渡していくことになります。

BaseComponent // count を表示する。 setCount を持っている。
├ Child1Component // count を上げたい
└ Child2Component
    └  GrandChildComponent // count を下げたい

極端ですが、上記の例の場合は setCountBaseComponent -> Child1ComponentBaseComponent -> Child1Component -> GrandChildComponent の2つのルートで渡す必要があります。

BaseComponentuseState[count, setCount] を作ります。他のコンポーネントは PropssetCount を受け取ります。 Child2Component なんかは countsetCount も関係ないのに、孫( GrandChildComponent )に setCount を渡すために PropssetCount を受け取る必要があるわけです。

これがReact界隈でよく言われるところの「バケツリレー」です。

アプリ全体で State 管理する Redux の登場

このバケツリレーが辛い場合、アプリ全体で State 管理してくれる機能が欲しくなります。 そのひとつが Redux です。

Redux を簡単に使える Redux-Toolkit の登場

この Redux が意外と厄介でわかりにくいものになっています。 概念を理解するのが大変で、それを実現するコードも複雑に感じられるものになっています。

で、概念だけは知っておく必要がありますが、コードは簡単に書きたい。そこで作られたのが Redux-Toolkit です。

以降、コードを書くと長い解説になってしまうのでここでは Redux-Toolkit を使うことを前提とした Redux の基礎知識と要点だけ書きます。

State を扱う

やりたいのは状態を保持し、必要に応じて状態を変更することです。以降、Redux が管理する状態を State と記載します。

5つの登場人物

Redux の話の中でよく出てくる登場人物です。

  • State : 状態。今回管理したいものです。
  • Action : State の変更指示内容です。
  • Store : State, ActionCreator, Reducer を保持します。まずはこれを作ります。
  • ActionCreator : Action を作る関数です。
  • Reducer : Action を受け取り State を変更します。

Store の中に他の4つすべて入っているイメージでOKです。 Reducer, Action, ActionCreator は初出の概念で分かりにくいので次で解説します。

処理の流れ

ボタンを押したら状態(State)が変わる、みたいな処理を作りたいと思います。 そのボタンがあるのは React で作っているコンポーネント(= Component)です。 以下は、その Component からスタートしてどのように処理が流れるかの図です。

Component 
    ↓ 呼び出す
ActionCreator(Action を作る関数です)
    ↓ Actionを渡す
Reducer(State を変更します)
    ↓ 変更する
State
    ↓ 参照される
Component

何を作るか?

Redux を使って作り込むのは Store です。前述しましたが、 Store に State , ActionCreator , Reducer が含まれています。 これらを作る関数が Toolkit から提供されています。それが createSlice です。

コンポーネントは何をするか?

React で作るコンポーネントからは以下の2つを行います。

  • 状態を見る → State の参照
  • 状態を変更する → ActionCreator を呼び出す

この2つを行うための関数は React-Redux から提供されています。

  • useSelector : State の参照
  • useDispatch : ActionCreator を呼び出す

これらを踏まえて...

これらの基礎概念だけ知っていれば、後は他のサイトのサンプルソースを読んで作り込みができるのではないかと思います。

redux-toolkit.js.org

まずは基本の公式サイト。

www.hypertextcandy.com

この記事は分かりやすかったですね。特に createSlice の図がありがたい。これだけでも価値ありです。

クラスコンポーネント

関数コンポーネントが充実しているんでそれでいいんじゃないかと思ったりしますが、クラスでもコンポーネントが作れるので簡単に紹介。(歴史的にはクラスでしかコンポーネントが作れなかったのが、関数コンポーネントが発展してきたという経緯があります。状態はクラスでしか作れなかったのですが、 Hooks のおかげでクラスである必要がなくなりました。)

props

HTMLタグにある、 id , class , src , href のような属性を独自に作るものです。これらの属性と同様、利用箇所(呼び出し元)から固定の値が渡されます。

class Hello extends React.Component {
  msg = "initial message." // msg属性に指定がなかった場合の初期値
  constructor(props) {
    super(props);
    this.msg = props.msg;
  }
  render () {
    return (
      <p>{this.msg}</p>
    );
  }
}

function App() {
  return (
      <Hello msg="この値が「this.msg」の部分に渡される" />
  );
}

state

props とは違い、利用箇所(呼び出し元)から変更可能な値です。クラスに state として保持します。変更時は setState() で新しい state を丸ごと設定し直すところが特徴的です。

class Hello extends React.Component {
  constructor(props) {
    super(props);
    // state は通常オブジェクトにします。
    this.state = {
      counter: 0
    };
    this.handleClick = this.handleClick.bind(this);
  }
  handleClick(e) {
    // setState で state を書き換えます。変更後のオブジェクトを設定します。
    this.setState((state)=>({
      counter: state.counter + 1
    }));
  }
  // onClick で handleClick メソッドを呼び出します
  render () {
    return (
      <div>
        <p>{this.state.counter}</p>
        <button onClick={this.handleClick}>plus</button>
      </div>
    );
  }
}

function App() {
  return (
      <Hello />
  );
}

context

決まった値を用意し、様々なコンポーネントで取り出すものです。 props と違うのは様々なコンポーネントで利用可能な値であること。グローバルな定数のイメージに近いです。 値を変更する場合は Provider を使います。 Themeなどに利用することを想定されているようです。

// context を用意します。
const data = 'initial message'; // 初期値
const ThemeContext  = React.createContext(data);

// <Hello /> の中で context を使います。
class Hello extends React.Component {
  static contextType = ThemeContext;
  render () {
    return (
      <p>{this.context}.</p>
    );
  }
}

function App() {
  const newData = 'new message';
  // Hello を呼び出すと初期値が使われます。
  // Provider を通すと Hello 内の context を変更できます。
  return (
    <div className="App">
      <Hello />
      <ThemeContext.Provider value={newData}>
        <Hello />
      </ThemeContext.Provider>
    </div>
  );
}