※スマホ対応はしてません。

カテゴリー: Web

React + TypeScriptのESLintルールをカスタマイズしたり、Airbnbのやつを導入するぞ。

カテゴリー: JavaScript

何もしないでそのままでも十分かもしれない。

先にまとめ

  • 最近の react-scripts は ESLint を含んでいるので別途インストール不要
  • 設定は .eslintrc.js を作成して extends: ['react-app'] する
  • Airbnb のルール設定を使う場合はもうちょい頑張る
  • 元々 create-react-app してない場合も同じ

Airbnb ルール利用時の手順概要

  1. ESLint 初期化
  2. ESLint の設定を更新
  3. コードを recommended な状態へ対応

想定環境

  • Node.js v10.16
  • react-scripts v3.1
  • TypeScript v3.5
  • ESLint v6.1
  • @typescript-eslint/eslint-plugin v1.13

React アプリの準備

create-react-app でテンプレートから作成します。既に存在するなら省略。

$ create-react-app my-greate-project --typescript --use-npm

--typescript で JS ではなく TS でファイル生成してくれます。

また create-react-app は標準で yarn を使います。--use-npm を設定すると npm になります。これはお好みで。

ESLint はインストール不要

react-scripts の依存パッケージとして ESLint 等がインストールされます。実際コンソールでエラーや警告を目にしたこともあるでしょう。

というわけで特に作業は不要です。

単体で実行する場合は TS の拡張子を指定

ESLint 単体で実行することも可能です。CI に組み込んでおくと良いかもね。

ただし、ESLint はそのまま実行すると JS ファイルだけを見に行きます。--ext で TS のファイルの拡張子 (extension) を見るよう指定します。

$ npx eslint src/ --ext .ts,.tsx

NPM スクリプトに登録

長いコマンドを今後何度も打たなくて良いよう package.json に書いておきます。

   "scripts": {
+    "lint": "eslint src --ext .ts,.tsx",
     "start": "react-scripts start",

そしたら今後はこう。

$ npm run lint

もし自動修正 --fix をやりたい場合はこうです。

$ npm run lint -- --fix

ESLint のルールをカスタマイズする

react-scripts 標準のルールセットは eslint-config-react-app というパッケージになっています。これも react-scripts に組み込まれているので、そのままで良ければ何もする必要はありません。

カスタマイズする場合は設定を用意します。ESLint の設定は .eslintrc.js というファイルで行います。他の選択肢もあるけど、たぶんこれが一番。

標準のルールセットを踏襲する場合、最小構成はこういう内容になります。

  • .eslintrc.js
module.exports = {
  extends: [
    'react-app',
  ],
  rules: {
  },
};

rules 内に好みのルールを追加してください。

元のルールを知る

create-react-app のリポジトリーの、 packages/eslint-config-react-app/index.js がそれです。

冒頭のコメントを引用、翻訳しておきます。

Inspired by https://github.com/airbnb/javascript but less opinionated.

We use eslint-loader so even warnings are very visible. This is why we only use “WARNING” level for potential errors, and we don’t use “ERROR” level at all.

In the future, we might create a separate list of rules for production. It would probably be more strict.

The ESLint browser environment defines all browser globals as valid, even though most people don’t know some of them exist (e.g. name or status). This is dangerous as it hides accidentally undefined variables. We blacklist the globals that we deem potentially confusing. To use them, explicitly reference them, e.g. window.name or window.status.

https://github.com/airbnb/javascript にインスパイアされつつもうちょいマイルドです。

eslint-loader を使用しているため、警告であっても非常に可視的です。そのため潜在的なエラーにも “WARNING” レベルを利用しており、”ERROR” レベルは全く利用していません。

将来的には本番環境用にまた別のルールリストを作成するかもしれません。そちらはより厳格になるはずです。

ESLint の browser 環境(訳者註:設定の env のこと)は全てのブラウザーグローバルを valid と定めており、それにはほとんどの方がその存在に気づいていないもの(例えば name や status)も含まれています。これは非常に危険であり、未定義の値を気づかないうちに隠蔽してしまう可能性があります。我々が潜在的に不確かであると判断したグローバルはブラックリストに登録しました。それらを利用する場合は明示的に参照してください。(例:window.name、window.status)

『警告であっても非常に可視的』の件は、”ERROR” レベルを利用するとリントエラーの際に(コンパイルエラー等と同様に)画面いっぱいにエラーが出てくるようになるのがアレだって話です。

最後の紛らわしいやつらブラックリストはこちら。

ESLint 設定を初期化

Airbnb のやつがたぶん今一番人気のあるルールセットかなと思うんですが、こちらを利用する場合は、react-scripts は忘れて ESLint の初期化機能を使うのが良いかなと思います。

yarn の場合は 2 手間かかります。

npm の場合

eslint --init で、対話形式で初期設定をします。

$ npx eslint --init
? How would you like to use ESLint?
❯ To check syntax, find problems, and enforce code style

? What type of modules does your project use?
❯ JavaScript modules (import/export)

? Which framework does your project use?
❯ React

? Where does your code run?
❯ Browser

? How would you like to define a style for your project?
❯ Use a popular style guide

? Which style guide do you want to follow?
❯ Airbnb (https://github.com/airbnb/javascript)

? What format do you want your config file to be in?
❯ JavaScript

? Would you like to install them now with npm?
❯ Yes
...

これで package.json やロックファイルが更新され、.eslintrc.js が生成される。

(もう一度言うけど、yarn の人は yarn install をお忘れなく。npm の人は大丈夫。)

このタイミングでいったん git add しちゃうのお勧めします。今後はこの自動生成されたファイルを手動で更新していくので。

yarn の場合

npm ではなく yarn でやっている方は、事前に Airbnb ルールをインストールするのと、対話中の npm 利用を断るのとの 2 手間が変わります。

eslint-config-airbnb をインストールするのは、こう。

$ yarn add eslint-config-airbnb

この後に npm の方と同じく eslint --init で生成します。前項参照。ただし、最後の “Would you like to install them now with npm?” は “No” にしてください。

“Yes” にすると npm install が始まってしまいます。やっちゃった場合は Ctrl-C でキャンセルするか、間に合わなければ package-lock.json を削除しといてください。なおその後の yarn install は特に不要です。どうやら他に必要なものは揃っているようなので。

ESLint の設定を更新

eslint --init で生成された .eslintrc.js に設定を足す。(途中の @@ -20,6 +22,26 @@ は中略のことと思ってください。)

diff --git a/.eslintrc.js b/.eslintrc.js
@@ -2,9 +2,11 @@ module.exports = {
   env: {
     browser: true,
     es6: true,
+    jest: true,
   },
   extends: [
     'airbnb',
+    'plugin:@typescript-eslint/recommended',
   ],
   globals: {
     Atomics: 'readonly',
@@ -20,6 +22,26 @@ module.exports = {
   plugins: [
     'react',
   ],
+  settings: {
+    'import/resolver': {
+      node: {
+        extensions: ['.js', '.jsx', '.ts', '.tsx'],
+      },
+    },
+  },
   rules: {
+    '@typescript-eslint/indent': [
+      'error',
+      2,
+    ],
+    '@typescript-eslint/prefer-interface': 'off',
+    'react/jsx-filename-extension': [
+      'error',
+      { extensions: ['.jsx', '.tsx'] },
+    ],
+    'react/prop-types': 'off',
+    'spaced-comment': [
+      'error',
+      'always',
+      { markers: ['/ <reference'] },
+    ],
   },
};

TS と JS とを混在させているプロジェクトの場合、'react/prop-types': 'off', は overrides の方に書くと良いでしょう。詳細は ESLint のサイトで。

初期状態から recommended な状態へ対応

頑張ってぽちぽち直していきましょう。特に serviceWorker.ts は大量に出てきてビビるんだけど、実はエラーの種類は多くないので、落ち着いてやれば大丈夫です。

src/App.tsx

例としてこいつだけ修正内容を載せておきます。元の状態は create-react-app で生成した直後のやつです。

diff --git a/src/App.tsx b/src/App.tsx
@@ -1,14 +1,19 @@
-import React from 'react';
+import React, { ReactElement } from 'react';
 import logo from './logo.svg';
 import './App.css';
 
-const App: React.FC = () => {
+// eslint-disable-next-line arrow-body-style
+const App: React.FC = (): ReactElement => {
   return (
     <div className="App">
       <header className="App-header">
         <img src={logo} className="App-logo" alt="logo" />
         <p>
-          Edit <code>src/App.tsx</code> and save to reload.
+          Edit
+          {' '}
+          <code>src/App.tsx</code>
+          {' '}
+          and save to reload.
         </p>
         <a
           className="App-link"
@@ -21,6 +26,6 @@
       </header>
     </div>
   );
-}
+};
 
 export default App;

arrow-body-style は、今後開発を進めたらきっと {} が必要になるので、一時的に無効化しています。必要ないならいいです。

ESLint のエラーと対処

今回の作業で出会うかもしれないエラーです。

No files matching ‘src’ were found.

検証対象のファイルがひとつもない。(対象外のファイルはあるかもしれない。)

ESLint はそのままでは JS ファイルだけを探すので、「NPM スクリプト準備」の項でやったように --ext で TS の拡張子を設定してやる。

Unable to resolve path to module ‘./App’. (import/no-unresolved)

import しているファイルが見つからない。

ESLint はそのままでは JS ファイルだけを探すので、「ESLint の設定を更新」の項でやったように settings['import/resolver'].node.extensions で TS の拡張子を設定してやる。

ちなみに ./App.tsx のようにした場合は TS のコンパイラーに怒られる。

An import path cannot end with a ‘.tsx’ extension. Consider importing ‘./App’ instead.
ts(2691)

JSX not allowed in files with extension ‘.tsx’ (react/jsx-filename-extension)

JSX の記法が、許可されていない拡張子のファイルで利用されている。

.jsx じゃないと使えないようになっているので、.tsx も許可する。「ESLint の設定を更新」を参照。

よくわかんないけどこれ recommended に入らないかな。

Missing return type on function. (@typescript-eslint/explicit-function-return-type)

関数の戻り値が明示されていない。

なくても TS として正しいけれど、’plugin:@typescript-eslint/recommended’ は明示を推奨している。

React.FC 戻り値は ReactElement 。「初期状態から recommended な状態へ対応」を参照。

他はだいたい () => … を (): void => … にすれば解決する。そうでないものはちゃんと実装されている戻り値を確認しよう。

Unexpected block statement surrounding arrow body; move the returned value immediately after the =>. (arrow-body-style)

アロー関数で return を省略可能なのにされていない。

() => { return 123; } を () => 123 にする。

App のやつは、開発を進めたらきっと {} が必要になるので一時的に無効化しておけば良いかなと思う。「初期状態から recommended な状態へ対応」を参照。

Expected exception block, space or tab after ‘//’ in comment. (spaced-comment)

行コメント開始の記号 // の直後に空白が置かれていない。

ただ TS では Triple-Slash Directives ↓というのがあって、/// を使ってコンパイラーへ指示 (directive) を与えることができる

具体的には src/react-app-env.d.ts がそれです。

/// <reference types="react-scripts" />

この /// がこのルールと衝突するので、例外として設定してやる必要がある。「ESLint の設定を更新」を参照。

‘checkValidServiceWorker’ was used before it was defined. (@typescript-eslint/no-use-before-define)

関数等が定義より先に利用されている。

順序を変えるか、オフにしちゃっても良いかも。

Assignment to property of function parameter ‘registration’. (no-param-reassign)

受け取った引数(あるいはそのプロパティ)を上書きしている。

serviceWorker.ts の場合は特に問題ないので、その行は無視する。

+      // eslint-disable-next-line no-param-reassign
       registration.onupdatefound = (): void => {

Use an interface instead of a type literal. (@typescript-eslint/prefer-interface)

type より interface を使おうってやつ。

設定でオフにしちゃおう。「ESLint の設定を更新」を参照。

というのも、「別に type でよくね? むしろ type の方がよくね?」というのがどうやら最近の流れなので。実際 @typescript-eslint/eslint-plugin の次のメジャーバージョン v2 では recommended から外れるようです。

Unexpected console statement. (no-console)

デバッグ用コードである console.log() 等が残っている。

故意に残す場合、その行を無視する。

+// eslint-disable-next-line no-console
 console.log('Here');

serviceWorker.ts の場合は全体的に出していきたい感じ?っぽいので、ファイル全体を無効化することにする。

+/* eslint-disable no-console */

‘xxx’ is missing in props validation (react/prop-types)

React の props の検証が行われていない。

TS プロジェクトの場合はコンパイラーが静的に検証してくれるので、オフにする。「ESLint の設定を更新」を参照。

An error occurred while generating your JavaScript config file.

ESLingの設定ファイル生成に失敗した。

npm ではなく yarn を使う場合、eslint --init より先にAirbnbの設定をインストールしておく必要があるようです。「ESLint の設定を更新」を参照。

エラー詳細はこちら。

An error occurred while generating your JavaScript config file. A config file was still generated, but the config file itself may not follow your linting rules.
Error: Failed to load config "airbnb" to extend from.
Referenced from: BaseConfig
Error: An error occurred while generating your JavaScript config file. A config file was still generated, but the config file itself may not follow your linting rules.
Error: Failed to load config "airbnb" to extend from.
Referenced from: BaseConfig
    at configMissingError (/path/to/project/node_modules/eslint/lib/cli-engine/config-array-factory.js:233:9)
…

おまけ:VS Code のプラグイン設定

ESLint を CLI から毎回実行するより、エディター上で確認して対応していく方が楽です。

VS Code の場合はプラグイン導入で実現できます。

TypeScript に対応させるには設定が必要です。VS Code の設定 JSON ファイルを開いて(Ctrl + , → 右上 “{}” ボタン)、こんな設定。

{
…
    "eslint.autoFixOnSave": true,
    "eslint.validate": [
        "javascript",
        "javascriptreact",
        {"language": "typescript", "autoFix": true },
        {"language": "typescriptreact", "autoFix": true }
    ],
…

“autoFix” はファイル保存時に自動で修正(できる場合に)してくれる機能です。好みだけども ON にしとくと良いと思います。TS の場合はこう↑書いておかないと利かない。

[解決済み] TypeScript が新しすぎる警告

(2019/08/11 追記)最新版で解決済み。

こんなん言われる言われていた。

WARNING: You are currently running a version of TypeScript which is not officially supported by typescript-estree.

You may find that it works just fine, or you may not.

SUPPORTED TYPESCRIPT VERSIONS: >=3.2.1 <3.5.0

YOUR TYPESCRIPT VERSION: 3.5.3

Please only submit bug reports when using the officially supported version.

よくわからない。既に解決済みのようだけど。

The update was merged on 6 Jun as you can see above, and it was released to the latest tag on npm on the 9th of June: https://github.com/typescript-eslint/typescript-eslint/releases/tag/v1.10.1

使ってるのは v1.13 なんだけどなあ。

おわり

linter は早めに入れるのがお得です。

更新履歴

  • 2019/08/31
    • yarn 用の ESLint 導入手順が不足だったのを修正
  • 2019/08/18
    • eslint-config-react-app を利用する方法を追加
    • 上記に合わせ記事タイトルを変更
    • 同じく全体の構成を変更
  • 2019/08/11
    • ESLint 及び @typescript-eslint/eslint-plugin のインストール作業を削除し、代わりに不要である旨記述
    • plugin:react/recommended を削除
    • 不要な TypeScript 系の設定を .eslintrc.js から削除
    • react-scripts を v6.0 から v6.1 へ更新
    • 不要になった @typescript-eslint/prefer-interface を削除
    • 「TypeScript が新しすぎる警告」が最新版で解決済みである旨記述
    • react/prop-types の対処を追加
  • 2019/08/06
    • 公開

参考

npm installやnpm ciがlength of undefinedで失敗するやつ、name設定で解決した。

カテゴリー: JavaScript

先に結論

エラー:

  • Cannot read property ‘length’ of undefined

条件:

  • npm v6.7+
  • package.json に postinstall 等があり、かつ name がない場合
  • npm install や npm ci 実行時

対処:

  • とりあえず package.json で name の設定すれば通る

あらすじ

TravisCI の自動試験で失敗の通知。

今まで Node.js v10.15 だったのが v10.16 になって失敗するようになった。過去に通ったやつも再試行すると失敗する。ひゃあ。

$ npm ci 
npm ERR! Cannot read property 'length' of undefined

debug.log はこんな感じ。

...
2533 info lifecycle eslint-config-airbnb-base@13.0.0~install: eslint-config-airbnb-base@13.0.0
2534 info lifecycle eslint-config-airbnb-base@13.0.0~postinstall: eslint-config-airbnb-base@13.0.0
2535 info lifecycle undefined@undefined~install: undefined@undefined
2536 info lifecycle undefined@undefined~postinstall: undefined@undefined
2537 verbose stack TypeError: Cannot read property 'length' of undefined
2537 verbose stack     at _incorrectWorkingDirectory (/home/ginpei/.nvm/versions/node/v10.16.0/lib/node_modules/npm/node_modules/npm-lifecycle/index.js:100:60)
2537 verbose stack     at /home/ginpei/.nvm/versions/node/v10.16.0/lib/node_modules/npm/node_modules/npm-lifecycle/index.js:72:44
2537 verbose stack     at /home/ginpei/.nvm/versions/node/v10.16.0/lib/node_modules/npm/node_modules/npm-lifecycle/index.js:203:12
2537 verbose stack     at /home/ginpei/.nvm/versions/node/v10.16.0/lib/node_modules/npm/node_modules/graceful-fs/polyfills.js:285:20
2537 verbose stack     at FSReqWrap.oncomplete (fs.js:154:5)
2538 verbose cwd /mnt/c/Users/ginpei/projects/webextension-pomodoro
2539 verbose Linux 4.4.0-17763-Microsoft
2540 verbose argv "/home/ginpei/.nvm/versions/node/v10.16.0/bin/node" "/home/ginpei/.nvm/versions/node/v10.16.0/bin/npm" "ci"
2541 verbose node v10.16.0
2542 verbose npm  v6.9.0
2543 error Cannot read property 'length' of undefined
2544 verbose exit [ 1, true ]

undefined@undefined とは……。

原因

手元で動かして追ってみる。

ということで、落ちる行まで追えた。

npm の実装は見たことないので細かいことわからないけど、ここの pkg.name.length が大丈夫なら解決しそう。

あと postinstall 削っても動いたので、ライフサイクル回りが npm v6.9 までに何か変わったのかも。

対処

というわけで、package.json で name を追加するとビルドが通るようになった。

本当なら報告したいけど Issue 作成は開かれてないし、この機に触ってみて PR 投げて貢献しようかな……と思って一月経ちました。

Firebaseのfirestore.indexes.jsonの書式とかインデックスの作り方とかで戸惑った。

カテゴリー: サーバー

先にまとめ

  • たぶん先にウェブコンソールで編集 → 結果を JSON へ保存、という流れ
  • firebase firestore:indexes で「現在設定されているインデックス」をJSONで得られる
  • なので、書式は不明だけど気にする必要なさそう
  • エラーメッセージにインデックス作成 URL が含まれる(のでそれを使う)

ここまでのあらすじ

一覧を新しいもの順にしようと orderBy() 追加したらエラーになった。

const collRef = firebase.firestore().collection(collectionName)
  .where('userId', '==', userId)
  .orderBy('updatedAt', 'desc');
FirebaseError: The query requires an index. You can create it here: https://console.firebase.google.com/project/your-project/database/firestore/indexes?create_composite=…

このクエリーにはインデックスが必要だよ、この URL から作れるよ。とのこと。

インデックスを作成する

firestore.indexes.json が気になるところなんだけど、後回し。

エラーメッセージに含まれる URL を開くと Firestore の Indexes のページが出てきて、「インデックス作るよ!」というダイアログが表示される。

Create composite index というダイアログ。これから作成するインデックスの対象コレクション、フィールドと昇順、降順が確認できる。

素直に “Create index” ボタンを押せばすぐ作ってくれる。

いや、すぐは作ってくれない

すぐ作り始めてはくれるんだけど、なかなか時間がかかる様子。

更新が必要なデータ量によっては、インデックスの作成に数分かかる場合があります。

手元の 10 件ない程度の環境で、完了まで 30 秒くらいにかかった。

インデックスを firestore.indexes.json で管理する

後から環境を再現できるようリポジトリーに設定を持っておきたい。わかる。

firebase init で firestore.indexes.json というファイルを作ってくれるんだけど、どうやらこのJSONファイルをいじるのではなく、先にコンソールで作業してから記録するというのが良さそう。

つまりこんな流れ。

  1. ウェブのコンソールで作業(前節でやった)
  2. firebase で定義のスナップショットを firestore.indexes.json へ保存
  3. 必要に応じて firebase からデプロイして環境を再現

スナップショットを保存

スナップショットという表現で良いのかわかんないけども。

firebase コマンドで現在設定されているものをJSONで得られる。標準出力に出てくるので、> を使ってJSONファイルへ上書き出力してやる。

$ firebase firestore:indexes > firestore.indexes.json

結果、例えばこんな感じであるらしい。

{
  "indexes": [
    {
      "collectionGroup": "rooms",
      "queryScope": "COLLECTION",
      "fields": [
        {
          "fieldPath": "userId",
          "order": "ASCENDING"
        },
        {
          "fieldPath": "updatedAt",
          "order": "DESCENDING"
        }
      ]
    }
  ],
  "fieldOverrides": []
}

デプロイして環境再現

本番環境を更新するとか開発環境を作り直すとかは、JSON ファイルを用意したあとは普通にデプロイするだけ。インデックスだけの場合は --only firestore:indexes で。

$ firebase deploy --only firestore:indexes

最初間違えて単数 firestore:index とやってしまい、でもエラーにならないので気づくのに時間がかかった……。

なお JSON へ追加してデプロイすると作成してくれるけど、JSON で削除してデプロイしても実際に削除してくれない。

コマンド詳細

$ firebase firestore:indexes --help
Usage: firestore:indexes [options]

List indexes in your project's Cloud Firestore database.

Options:
  --pretty    Pretty print. When not specified the indexes are printed in the JSON specification format.
  -h, --help  output usage information

CI でデプロイするとしばらくエラーに

CI から firebase deploy するプロジェクトで試したところ、Firestore の更新も Hosting へのアップロードも同時に行われるため、Firestore のこのインデックス作成完了までの間はエラーになってしまった。なんか対応のしようがあるんだろうか? いったん firebase deploy --only firestore:indexes して、準備完了をどうにかして検知して、それから Hosting を更新するなんてことができる?

削除して再作成

削除はコンソールで Firestore → Indexes の一覧右側 “…” から。すぐ終わる。

終わるが、そのあとすぐに作り直そうとするとエラーになった。

$ firebase deploy --only firestore:indexes

=== Deploying to 'my-project'...

i  deploying firestore
i  firestore: reading indexes from firestore.indexes.json...

Error: HTTP Error: 409, index already exists

たぶんサーバー側の中のどこかでキャッシュみたいなものが残ってるんではなかろうか。知らんけど。

3 分程度間をおいて再実行したら成功した。作成と同様、量によって時間が伸びると思う。

セキュリティルールは先に JSON を書く

じゃあインデックスだけじゃなくてセキュリティルールも先にコンソールでやるのか、と思ったけどどうもそうでもないらしい。

firebase --help で出てくるコマンド一覧には、Firestore 関連のものはこの indexes と delete のふたつだけ。やっぱりコンソールで試して → JSON コピペして保存して → デプロイ、という流れか。

おしまい

参考

更新履歴

  • 2019-06-01 初版
  • 2019-06-10 「CI でデプロイするとしばらくエラーに」追加

Reactで既存HTMLのプロパティを受け取るのにComponentPropsWithRefが使える?

カテゴリー: JavaScript

話題になっているのを全然みかけないんだけど、皆なんかもっと良いやり方やってんの? こういうことしないの?

あとコードハイライトなくてごめんね……。

先にまとめ

  • TypeScript の話
  • 既存 HTML ほとんどそのままのコンポーネントで、本来の props をそのまま受け取りたい場面
  • props: React.ComponentPropsWithRef<'span'> みたいにやれるっぽい
  • ドキュメントには載ってない
  • 定義を追っかけてみたけどたぶんきっと大丈夫そう
  • よくわかってないです

こんな風にして使っている。

export type HtmlProps<T extends ElementType> =
  React.ComponentPropsWithRef<T>;

export type HtmlComponent<T extends ElementType> =
  React.FC<HtmlProps<T>>;

後者は毎回書くのが面倒で追加したけど、ない方がコードは読みやすいかもしれない。

これを使って、特定のクラス名を付けただけのHTML要素を返すコンポーネントの例。

export const NiceButton: HtmlComponent<'button'> = (props) => (
  <button
    {...props}
    className={`NiceButton ${props.className || ''}`}
  />
);
<NiceButton>Niceですね~</NiceButton>

もうちょい複雑なコンポーネントの例。

interface NiceCheckboxProps extends HtmlProps<'input'> {
  label: string;
}

export const NiceCheckbox: React.FC<NiceCheckboxProps> = (props) => {
  const { className = '', label, ...inputProps } = props;
  return (
    <label className={`NiceCheckbox ${className}`}>
      <input {...inputProps} className="NiceCheckbox-input" type="checkbox" />
      <span className="NiceCheckbox-label">{label}</span>
    </label>
  );
};
<NiceCheckbox
  checked={agreed}
  label="ナイスであることに同意します"
  onClick={onAgreeClick}
/>

場面

ほぼ HTML

上に書いた、ああいう「ほとんどただの HTML 要素なんだけどちょっと追加とか組み合わせとかがあって、方々で使うから名前を付けて簡単に使いまわしたい」ような場面です。

CSS フレームワークみたいなノリだと思うんだけど、React でやるからには毎回クラス名書くんじゃなくて、そのクラス名を付けてくれるコンポーネントを使うようにしたいなと。

HTML同様の型が欲しい

例えば <input> だと value を持ってたりするじゃないですか。<span> にはないじゃないですか。そういうのを、補完で出したり型確認で弾いたりしたいなと思っておりました。

見つけた

React.prop まで入力したところの補完で出てきた候補の中からそれっぽいのを眺めてるときに見つけました。

React 公式サイトで検索しても出てこない、と思ったけど React プロジェクトじゃなくて @typed のだから当たり前か。ありがてえ。

定義を追ってみる

ここから先はTS初級者がうんうん唸ってるだけです。誰かに教えてほしい……。

なお @types/jest のバージョンは 24.0.12 でした。

ComponentPropsWithRef

type ComponentPropsWithRef<T extends ElementType> =
    T extends ComponentClass<infer P>
        ? PropsWithoutRef<P> & RefAttributes<InstanceType<T>>
        : PropsWithRef<ComponentProps<T>>;

へ、へえー……。

ElementType

まずジェネリクスの方。

type ElementType<P = any> =
    {
        [K in keyof JSX.IntrinsicElements]: P extends JSX.IntrinsicElements[K] ? K : never
    }[keyof JSX.IntrinsicElements] |
    ComponentType<P>;

これはあれだな、IntrinsicElements にあればその型('a' なら <a> 用の props)を使うぞってことだな。

IntrinsicElements

interface IntrinsicElements {
    // HTML
    a: React.DetailedHTMLProps<React.AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement>;
    abbr: React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>;
    address: React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>;
    area: React.DetailedHTMLProps<React.AreaHTMLAttributes<HTMLAreaElement>, HTMLAreaElement>;
    article: React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>;
…

究極的にはここの右側 React.DetailedHTMLProps<React.AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement> を毎回コピペして使えば使えるんだな。したくないけど。

JSX で書いた <span> とかを VS Code で Ctrl + クリックすると出てくるのもこいつ。

ComponentClass

こっちがわからない。

interface ComponentClass<P = {}, S = ComponentState> extends StaticLifecycle<P, S> {
    new (props: P, context?: any): Component<P, S>;
    propTypes?: WeakValidationMap<P>;
    contextType?: Context<any>;
    contextTypes?: ValidationMap<any>;
    childContextTypes?: ValidationMap<any>;
    defaultProps?: Partial<P>;
    displayName?: string;
}

<infer P> との組み合わせで、React.Component 継承のクラスを与えると良い感じに props の型を抽出してくれる系かなと思ったんだけどそんなことなかった。

ちなみに infer ってここ(上の ComponentPropsWithRef のとこ)で初めて見ました。

PropsWithRef

Indent Hadouken みがある。

/** Ensures that the props do not include string ref, which cannot be forwarded */
type PropsWithRef<P> =
    // Just "P extends { ref?: infer R }" looks sufficient, but R will infer as {} if P is {}.
    'ref' extends keyof P
        ? P extends { ref?: infer R }
            ? string extends R
                ? PropsWithoutRef<P> & { ref?: Exclude<R, string> }
                : P
            : P
        : P;

たぶん大丈夫でしょう

知らんけど。

とりあえず動いてはいる。うーんちゃんと理解して使いたい……。

おしまい

VS Code で F12 を駆使して追っかけてみました。ほんとべんり。

よくわからないけどとりあえず動いているっぽいのでこのまま使おう。趣味プロジェクトだし、まあーそのうちTS力が上がれば完全理解できるでしょう。

あとそういえばこのブログテーマも更新して、TypeScript のハイライトも入れたいなあ。

Edgeでiframe内でウェブフォントが使えないかもしれない問題を踏んだ。

カテゴリー: CSS

最近踏んだ問題です。

  • Edge
  • 複数の <iframe>
  • フレームは同時に生成
  • 同じウェブフォントを読み込む
  • 一部のフレームでしかウェブフォントが出てこない

デモとコードはこちら。

3つのフレームのうちひとつでしかウェブフォントが有効化されてない様子。

EdgeのDevToolsでネットワーク見てると、CSSファイルは読み込まれているけどフォントファイルは読み込まれていない、という状況までは観測できた。

解決策

どうやらCSSファイルの読み込み完了を待ってから、そのウェブフォントを利用する部分を出力すると問題なくなる様子。

つまり onload で待つべしと。

const elLink = document.createElement('link');
elLink.href = `https://fonts.googleapis.com/css?family=${family}`;
elLink.rel = 'stylesheet';

elLink.onload = () => render(); // <---

document.body.appendChild(elLink);

原因はっきりわかってないけどこれでうまく行っているように見える。

処理の想像。

  1. フレーム1出力開始
    1. CSSファイル読み込み開始
    2. ウェブフォント利用のテキスト出力
    3. 該当フォントなしとして記憶 (A-1)
  2. フレーム2出力開始
    1. CSSファイル読み込み開始
    2. ウェブフォント利用のテキスト出力
    3. 該当フォントなしとして記憶 (A-2)
  3. いずれかのフレームで
    1. CSSファイル読み込み完了 (B)
    2. 該当なしのフォントに合致、フォントファイル読み込み開始 (C)
    3. フォントファイル読み込み完了
    4. テキスト再描画

このB前にAが来るとCがちゃんと動いてない気がします。知らんけど。

Edge…

老い先短いEdgeHTMLさんのためにどれだけコストかけるべきか悩む。

おまけ

おまけ2