かもメモ

自分の落ちた落とし穴に何度も落ちる人のメモ帳

TypeScript FormData.entries() で取得したフォームデータの型を interface から作成したい

TypeScript な React のフォームを作成していて new FormData() で取得したフォーム内容のデータの型を送信する形式の interface から作成したかったのメモ

FormData で取得されるデータの形式

const formData = [...new Form(form).entries()];
// => [[name1, value1], [name2, value2], ...]

new Form(form) は FormData オブジェクトなので、フォームの namevalue を扱うために FormData.entries() したものを使う。
FormData.entries() はフォームの [name, value] のセットの配列を返す。

interface から FormData.entries() の返す配列の型を作成する

interface

interface IFromSchema {
  name: string;
  email: string;
  amount: number;
  isAgree: boolean;
}

FormData.entries() の返す形式が [name, value][] なので、nameIFromSchema の key、value の型は IFromSchema[key] で最終的に次のような型ができれば良さそう

type IFromSchemaEntries = [
  ['name', string];
  ['email', string];
  ['amount', number];
  ['isAgree', boolean];
];

タプルの方が良いのかな?

Mapped Types を利用して Entries で作成される型を作る

type Entries<T> = {
  [K in keyof T]: [K, T[K]];
}[keyof T][];

👇

type T1 = {
  [K in keyof IFromSchema]: [K, IFromSchema[K]]
};
// T1 は次のようなオブジェクト形式の型になる
// {
//   name: [name, string],
//   email: [email, string],
//   amount: [amount, number],
//   isAgree: [isAgree, boolean],
// }

type T2 = T1[keyof IFromSchema];
// T2 はオブジェクトに[key] でアクセスしている形なので次用な型になる
// => type T2 = [name, string] | [email, string] | [amount, number] | [isAgree, boolean];
// 配列 or タプル形式になっていれば良いので

type T3 = T1[keyof IFromSchema][];
// => type T3 = ([name, string] | [email, string] | [amount, number] | [isAgree, boolean])[];
// key と key に対応した型がセットになったタプル型の配列になる

// 抽象化して
type Entries<T> = {
  [K in keyof T]: [K, T[K]];
}[keyof T][];

type FormEntriesType = Entries<IFromSchema>;
// => ([name, string] | [email, string] | [amount, number] | [isAgree, string])[];

これで Object.entries() したときの型が Object の interface から作れつようになりました!
但し、([name, string] | [email, string] | [amount, number] | [isAgree, string]) なので確実に全ての key が含まれているとは限らない配列ということになると思います。Form のチェックボックスとかチェックがない場合 FromData に含まれないからこの方が扱いやすいかも!?

FormData で使ってみる

const handleFormSubmit = (evt: React.FormEvent<HTMLFormElement>) => {
  evt.preventDefault();
  const form = formRef.current;
  if (!form) {
      return;
  }

  const FormDataEntries = [...new Form(form).entries()] as  Entries<IFromSchema>;
  
  // Entries<IFromSchema> はキーが不足していても通るのでバリデーションエラーになるデフォルトのデータを用意しておく
  const defaultData: IFromSchema = {
    name: '',
    email: '',
    amount: 0,
    isAgree: false,
  };
 
  // IFromSchema 形式に戻してvalidation / post する
  const postData = FormDataEntries.reduce((data, item) => {
    const [key, value] = item;
    return {
      ...data,
      [key]: value,
    };
  }, defaultData);

  if (validate(postData)) {
    // フォームを送信
  }
};

[...new Form(form).entries()] as Entries<IFromSchema>as で型を付けているので完全に安全な訳ではないのでバリデーションでチェックする必要があるとお思いますが、型補完が効いて扱いやすくなりました!

所管

本当にこの方法が良いのか解ってない部分がありますが、実現したいことはまぁできたかな〜という感じ。こうやって調べている中で TypeScript のジェネリックもチョットづつ分かってきた気がします!
₍ ᐢ. ̫ .ᐢ ₎👌 TIL (Today I Learned) 少しづつ強くなる


[参考]

めばちさんの絵ホント好き (ネタ画像ネタ切れ!)

React HTML の HEAD 情報を書き換える react-helmet 使おうとしたら警告が出た件

React で <head> の情報を書き換えるのに react-helmet を使った所 Warning: Using UNSAFE_componentWillMount in strict mode is not recommended and may indicate bugs in your code. という警告が発生してしまいました。

環境

  • "react": "^17.0.2",
  • "react-helmet": "^6.1.0"

react-helmet で発生した警告

import { VFC } from 'react';
import { Helmet } from 'react-helmet';

export const App: VFC = () => {
  return (
    <>
      <Helmet>
        <title>{PAGE_TITLE}</title>
        <script src={SCRIPT} />
      </Helmet>
      <div className="app">{/*略*/}</div>
    </>
  );
};

👇

Warning: Using UNSAFE_componentWillMount in strict mode is not recommended and may indicate bugs in your code. See https://fb.me/react-unsafe-component-lifecycles for details.
* Move code with side effects to componentDidMount, and set initial state in the constructor.
Please update the following components: SideEffect(NullComponent)

React 16.3 以降 componentWillMount メソッドは安全でないとして非推奨になっていた

  • 16.3: Introduce aliases for the unsafe lifecycles, UNSAFE_componentWillMount, UNSAFE_componentWillReceiveProps, and UNSAFE_componentWillUpdate. (Both the old lifecycle names and the new aliases will work in this release.)
  • A future 16.x release: Enable deprecation warning for componentWillMount, componentWillReceiveProps, and componentWillUpdate. (Both the old lifecycle names and the new aliases will work in this release, but the old names will log a DEV-mode warning.)
  • 17.0: Remove componentWillMount, componentWillReceiveProps, and componentWillUpdate . (Only the new “UNSAFE_” lifecycle names will work from this point forward.)

Here, “unsafe” refers not to security but instead conveys that code using these lifecycles will be more likely to have bugs in future versions of React, especially once async rendering is enabled.
cf. Update on Async Rendering – React Blog

componentWillMount, componentWillReceiveProps, componentWillUpdate は特に非同期レンダリングをするようになると誤用するとバグを発生させやすいので非推奨として、UNSAFE_ の付いたメソッドのエイリアスを作成し React v 17.0 で元のメソッドが削除となっているようです。

react-helmet では内部で使用しているライブラリ(react-side-effect)で UNSAFE_componentWillMount が使われているために、今回の警告が発生しているようでした。
cf. Stop using `UNSAFE_componentWillMount` · Issue #548 · nfl/react-helmet · GitHub

<head> の変更は react-helmet-async を使う

react-helmet relies on react-side-effect, which is not thread-safe. If you are doing anything asynchronous on the server, you need Helmet to encapsulate data on a per-request basis, this package does just that.
cf. react-helmet-async - npm

React Helmet を fork して作られた react-helmet-async はこの react-side-effect の問題を解決しているようです。そして react-helmet-async は TypeScript なので @types をインストールする必要もなく、Next.js でも使われているようです!(Download数も react-helmet-async の方が多いですね)

react-helmet-async の使い方

react-helmet-async は react-helmet とは少し異なり <Helmet> タグを使用するコンポーネント<HelmetProvider> で囲ってあげる必要があります。
先ほど警告の出ていたコードを修正します。

import { VFC } from 'react';
+ import { Helmet, HelmetProvider } from 'react-helmet-async';
- import { Helmet } from 'react-helmet';

export const App: VFC = () => {
  return (
+   <HelmetProvider>
-   <>
      <Helmet>
        <title>{PAGE_TITLE}</title>
        <script src={SCRIPT} />
      </Helmet>
      <div className="app">{/*略*/}</div>
+   </HelmetProvider>
-   </>
  );
};

これで Warning: Using UNSAFE_componentWillMount in strict mode is not recommended and may indicate bugs in your code. な警告は出なくなりました!
アプリ全体で使うなら src/index.tsx やアプリのルートになる src/App.tsx でアプリを丸っと囲ってあげれば良さそう。

結論

React で <head> を書き換えるときは react-helmet-async を使おう!
Class Component 時代を通ってこなかったので componentWillMount まわりのアレコレ全然把握してなかったのと、これからも React はどんどん変化していくだろうからちゃんとキャッチアップしておかないとな〜と感じました。

[参考]

Viva!公務員(字幕版)

Viva!公務員(字幕版)

  • ケッコ・ザローネ
Amazon
イタリア映画おもしろいです!

React create-react-app したプロジェクトで paths alias 使おうとして盛大にハマったメモ

create-react-app で作った TypeScript の React アプリで Next.js でやって便利だと感じたパスのエイリアス設定しようとして盛大にハマったのでメモ。

ゴール

create-react-app --template teypescript で作った TypeScript の React アプリで /src ディレクトリのパスのエイリアスを作成して、何処からでも import { XXXX } from '@/components/XXXX' のような形で import できるようにする。../ が連続する相対パスを使わなくて済むようにする

環境
react: ^17.0.2
react-scripts: 4.0.3
typescript: ^1.0.1
eslint: ^7.2.0
/root
 |- /.vscode
 |- /public
 |- /src
 |- .eslintignore
 |- .eslintrc.json
 |- .gitignore
 |- .prettierrc.json
 |- package.json
 |- README.md
 |- tsconfig.json
 |- yarn.lock

🐞 tsconfig.jsonpaths でパスのエイリアスを設定してもアプリ実行時にオプションが消されてしまう問題

Next.js では tsconfig.jsonbaseUrlPaths オプションでパスのエイリアスが設定できました。
同じ様に tsconfig.json に設定を書いてみました

# tsconfig.json
{
  "compilerOptions": {
    "target": "es5",
    "lib": ["dom", "dom.iterable", "esnext"],
+   "baseUrl": './src',
+   "paths": {
+     "@/*": ["./*"]
+   }

これでパスのエイリアスで import できると思ったのですが、 yarn start でアプリを起動すると tsconfig.jsonpaths オプションが丸っと削除されてしまいました。
どうやら react-scripts が自動的に tsconfig.json の内容を書き換えてしまうようです… どうして…

方針

  1. extends を使って別ファイルから paths のオプションを読み込ませる
  2. TypeScript は パスの解決は行わないので、webpack の設定を上書きしてパスの解決を行う
  3. ESLint でのエイリアスのパスを使っていると import/no-unresolved なエラーになる問題を解消する
  4. VS Codeエイリアスのパスで補完が効くようにする

1. extends を使って tsconfig.json に別ファイルから paths のオプションを読み込ませる

tsconfig.json"compilerOptions": {} の内容がビルド時に cleanup されるのは正常な挙動のようです。
このファイルで対処することができないので、 "extends" を使って別ファイルから設定を読み込ませるようにします。

# 読み込ませる設定ファイルを作成 (ファイル名は何でも良い)
$ touch tsconfig.paths.json

tsconfig.paths.jsonエイリアスで使いたいパスの設定を書く

{
  "compilerOptions": {
    "baseUrl": "./src",
    "paths": {
      "@/*": ["./*"]
    }
  }
}

tsconfig.jsontsconfig.paths.json を読み込ませる

{
+ "extends": "./tsconfig.paths.json",
  "compilerOptions": {

cf.

この状態で パスのエイリアスが使える様になった思ったのですが yarn start でアプリを起動すると @/components/XXXX Module not found というモジュールが読み込めないエラーが表示されてしまいました。

ビルドされたコードを見てみるとエイリアスのパス部分がそのまま @/componrnts/XXXX と出力されており、エイリアスのパスが正しいパスに解釈されていない = パスの解決がなされてないのが原因のようです。

2. TypeScript は パスの解決は行わないので、webpack の設定を上書きしてパスの解決を行う

tsconfigcompilerOptions.paths オプションで TypeScript のパスのエイリアスの設定はできるが、バンドルする際のパスの解決は TypeScript の責務ではないので、tsconfig に設定を書くだけではダメということっぽい。

create-react-app で作ったアプリは webpack でバンドルされているので、webpack の設定を上書きしてエイリアス設定しているパスの解決をする必要がある。eject することなく webpack の設定などを上書きすることができる react-app-rewired というライブラリを使ってエイリアス設定のパスを解決する設定を作成する。

$ yarn add -D react-app-rewired

ドキュメントを参考に上書きする設定ファイルを config-overrides.js という名前で作成する。
config-overrides.js にパス解決の設定を記述する

const path = require('path');

module.exports = (config) => {
  config.resolve = {
    ...config.resolve,
    alias: {
      ...config.alias,
      // エイリアスにするパスをココに書く
      '@': path.resolve(__dirname, './src/'),
    },
  };

  return config;
};

react の実行を react-scripts から react-app-rewired を変更して設定の上書きを読み込むように変更する

# package.json
  "scripts": {
-   "start": "react-scripts start",
-   "build": "react-scripts build",
-   "test": "react-scripts test",
-   "eject": "react-scripts eject",
+   "start": "react-app-rewired start",
+   "build": "react-app-rewired build",
+   "test": "react-app-rewired test",
+   "eject": "react-app-rewired eject",

config-overrides.js は設定ファイルでアプリからは import しないので .eslintignore に追加して ESLint の対象から外しておく (エラーが表示されてウザいので)

.eslintignore

/config-overrides.js

cf

これでパスの解決がされエイリアスで import できるようになったが、ESLint がエイリアスでのインポートに import/no-unresolved (Unable to resolve path to module '@/component/XXXX' import/no-unresolved) のエラーを表示するので修正する必要があります。ESLintくんの設定めんどい…

3. ESLint でのエイリアスのパスを使っていると import/no-unresolved なエラーになる問題を解消する

.eslintrc.jsparserOptions.project に tsconfig のパスを指定していても、paths の設定を理解させるには settings.import/resolver に TypeScript の設定が別途必要なようです。(理由まで調べてなし)

TypeScript 用の import/resolver のパッケージをインストール

$ yarn add -D eslint-import-resolver-typescript

.eslintrc.js に設定を追加

  settings: {
    'import/resolver': {
+     typescript: {
+       project: './tsconfig.json',
+     },
    },

設定は typescript: {} だけでも上手く動作するっぽい。(デフォルトで tsconfig.json を探してきて読み込むから?)

cf.

ここまで設定するとエイリアスのパスが問題なく使用できるようになり、/src の内のファイルからは何処からでも @//src として import のパスを書けるようになりました!

4. VS Codeエイリアスのパスで補完が効くようにする

目的は達成できていますが、エディタでパスの補完ができれば生産性がよくなるので VS Codeエイリアスのパスの補完が効くように設定します。

./vscode/setting.json

{
  "path-autocomplete.pathMappings": {
    "@": "${folder}/src"
  },
}

cf.

これで import from '@/' と書けば /src からのパスのサジェストが表示されるようになりました! ₍ ᐢ. ̫ .ᐢ ₎ つかれた〜

おわりに

Next.js だとこれだけだったのに…

Next.js のフレームワークがいい感じにしてくれてたのですね。感謝〜

モックとかで新規プロジェクト作ることが多いので一生環境作ってる気がします…


[参考]