Create React App + TypeScript + SCSS 環境を構築してみる

2016年に create-react-app 単体で試したことがあった。

あれから3年半が経過。React がもてはやされていた中で「Angular はちょっと重たいよねー」「とはいえ React も難解になってきたよねー」とか言っていると Vue.js の手軽さで SPA 全体が完全に主流になったと思う。Next.js や Nuxt.js、Angular CLI などの発展で、統一感のある CLI に沿った開発環境というのも安定してきたと思う。

そして何より、TypeScript を使った開発というのが、もはや「モダン」でもなんでもない「スタンダード」にすらなったかなと思う。

以前の記事では TypeScript などを試さなかったので、今回は最新の Create React App をベースに、TypeScript + SCSS を使った環境を作ってみる。そこで遭遇したトラブルも記録するのでご参考までに。

目次

Create React App でプロジェクトの雛形を作る

npm の npx コマンドを使えば、create-react-app のグローバルインストールは不要。もちろん、事前に $ npm i -g create-react-app としても問題はない。

# 以下のコマンドで雛形を作成する
$ npx create-react-app example-my-app --use-npm --typescript

Yarn が別に好きではないので、個人的な趣味で npm を使う --use-npm オプションを設定。

TypeScript ベースのプロジェクトにするため --typescript オプションを付けたが、ワーニングで「今後は --template typescript オプションを使え」と出ていた。今後は

$ npx create-react-app example-my-app --use-npm --template typescript

が雛形作成コマンドになるな。

$ cd ./example-my-app/
$ npm start

プロジェクトを作成したら、上のようにすることで http://localhost:3000/ に簡易サーバが起動する。

SCSS を書けるようにする

上述のように TypeScript ベースのプロジェクトを作っても、CSS 周りは標準的な CSS しかサポートされていない。

SCSS を書けるようにするには、node-sass をインストールしておくだけで良い。とてもお手軽!

$ npm install --save node-sass

node-sass をインストールするだけ。

あとは index.cssApp.css の拡張子を .scss にリネームし、import 部分も .css から .scss に直せば、自動的に SCSS のコンパイルを行って動作してくれるようになる。

Linux 環境で npm start 実行時に「System limit for number of file watchers reached」エラーが出た

Ubuntu 18.04 環境で React プロジェクトを立ち上げたところ、npm start 時に

Error: ENOSPC: System limit for number of file watchers reached,

このようなエラーメッセージが出た。

以下のとおり対応すれば良い。

# inotify が小さいと思われる
$ cat /proc/sys/fs/inotify/max_user_watches
8192

# 上限値を増やす
$ echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf
$ sudo sysctl -p

コレで npm start してもエラーが出なくなる。

package.jsonhomepage プロパティを指定すると npm start が正常に動作しなくなる

npm initpackage.json を生成すると、homepage プロパティが記録される。コレに合わせて、Create React App が作った package.json にも homepage プロパティを書いた。すると npm start 時に URL の階層構造が狂ってしまった。

どうやら React は package.jsonhomepage プロパティを見てパス解決を調整しているようなので、通常の npm パッケージの感覚でこのプロパティを書かないようにしないといけない。最終的にアプリが動作する URL、例えば GitHub Pages の URL などを書いておくのが適切だろう。

ビルドしたモノを動かす

npm start による簡易サーバ・ホットリロードではなく、一度ちゃんとビルドして、それを動作させてみる場合。serve というパッケージを使うのが公式で紹介されていた。

$ npm run build

$ npx serve -s ./build/
# http://localhost:5000/

npm start 時と npm run build 時とで homepage を変えたい

前述のとおり、package.jsonhomepage プロパティは、npm run build でビルドした資材を公開する場所に合わせておくことが望ましい。

しかしそれだと、npm start で開発したい時にこの URL が適用されてしまい、パス解決に不具合が出る場合がある。

その場合は、次のように PUBLIC_URL 環境変数を指定して npm start を実行すれば良い。

{
  "scripts": {
    "start": "PUBLIC_URL='.' react-scripts start"
  }
}

Service Worker を使わない場合は完全削除して良い

標準だと、index.tsx より serviceWorker.tsx がインポートされているが、いきなり有効化されないように、unregister() としかしていない。

// src/index.tsx 抜粋

import * as serviceWorker from './serviceWorker';  
serviceWorker.unregister();

Service Worker を使わないことが確定している場合は、これらの記述をすべて削除し、serviceWorker.tsx ごと削除しても問題なかった。

Component クラスの関数はアロー関数で書くとよさげ

イマイチ分かっていないのだが、Component クラスに独自に定義する関数について。

export default class App extends React.Component {
  // State の型を宣言しておく
  state: Readonly<any>;
  
  // 画面から呼び出す予定の関数を定義
  onSubmit() {
    this.setState({ hoge: 'FUGA' });  // ← コレができない
  }
}

上のように、関数を onSubmit() {} という風に定義し、this.setState() を呼び出して値を変更しようとすると、なぜか undefined となってしまう。

this が解決できていないんだろうなーとは思ったが、何でコレがダメなんだっけ?というのが分かっていない。

とりあず回避するには、

エラーを回避できる。

export default class App extends React.Component {
  // State の型を宣言しておく
  state: Readonly<any>;
  
  // 画面から呼び出す予定の関数を定義
  onSubmit = () => {
    this.setState({ hoge: 'FUGA' });  // ← コレなら OK
  }
}

条件付きレンダ

Component の render() メソッドは、最終的に JSX を return すれば良いので、return キーワードを書くまでに if 文を組み合わせてみたりしても良い。状態に応じて大きく HTML 構造が変わる場合は、if 文で処理して複数の return を書いたりした方がスッキリするかもしれない。

export default class App extends React.Component {
  render() {
    // ローディング中の HTML
    if(this.state.isLoading) {
      return (
        <div>Loading...</div>
      );
    }
    
    // ロード完了後の HTML…
    return (
      <div></div>
    );
  }
}

一方、JSX の中で条件分岐を行いたい場合は、「条件付きレンダ」で書けば良い。

return (
  <div>
    { messages.length === 0 &&
      <p>表示できるメッセージはありません</p>
    }
  </div>
);

ブレース {} 内は JavaScript の構文を直接書けるので、JavaScript のイディオムである &&|| の他、三項演算子を組み合わせたりもできる。覚えておこう。

kuromoji を使う

nemui-gacha-js を作った時のメモ。kuromoji という形態素解析ツールを React 内で使うためにやったこと。

まずはインストール。@types での型定義もあるので一緒にインストールしておくと良い。

$ npm install -S kuromoji
$ npm install -D @types/kuromoji

同梱の dict/ ディレクトリを、./public/dict/ でアクセスできるように移動しておく。

$ cp -a ./node_modules/kuromoji/dict/ ./public/

そうすれば、次のように書くことで kuromoji が使えるようになる。

import * as kuromoji from 'kuromoji';

kuromoji.builder({
  dicPath: './dict'  // 予め kuromoji の dict/ ディレクトリを public/ 配下にコピーしておく
}).build((error: Error, tokenizer: kuromoji.Tokenizer<any>) => {
  // tokenizer を使って処理…
});

ts-node のコンパイラオプションを上書きする

React ベースのプロジェクト内で、ちょっとだけ ts-node を使って CLI 向けスクリプトを動かしたい場面があった。そんな時、React 向けの tsconfig.json だと ts-node が動作しなかった。

調べてみると、次のように環境変数 TS_NODE_COMPILER_OPTIONStsconfig.json の設定を上書きしてやれば動作した。

$ TS_NODE_COMPILER_OPTIONS='{"module":"commonjs"}' ts-node HOGE.ts

package.json に定義するならこんな感じ。

{
  "scripts": {
    "exec-cli": "TS_NODE_COMPILER_OPTIONS='{\"module\":\"commonjs\"}' ts-node HOGE.ts"
  }
}

以上

こんな感じ~。TypeScript と SCSS が使えるようにできたので、Angular CLI や Vue CLI と同じく、React も Create-React-App で良い感じですね~。