READYFOR Tech Blog

READYFOR のエンジニアブログ

React のフォーム向けに ESLint プラグインを作った話

こんにちは。READYFOR でフロントエンドエンジニアとして働いている菅原(@kotarella1110)です! 最近 React のフォームライブラリとして React Hook Form を社内で採用することになり、コントリビュートしている身としてはかなりテンションが上がっております 🙌

話は変わりますが、先日 eslint-plugin-react-form-fields という ESLint プラグインを公開しました 🎉

ESLint プラグインを作ること自体初めてだったのですが、思ったより簡単に ESLint プラグインを作成できました。 そこで、本記事ではこの ESLint プラグインの作成経緯やプラグインの実装の話などについて記載します。

今回作成した ESLint プラグインについて

React のフォーム固有のルールがいくつか存在

React のフォームには固有のルールがいくつか存在します。 その中でも代表的なルールを一つ挙げると、React のフォーム要素は Controlled または Uncontrolled のいずれかである必要があります。 Controlled Components は React によって入力値を制御しますが、Uncontrolled Components は入力値を DOM 自身(ブラウザ)が制御します。 両者の比較や違いについてこちらに詳しく記載しておりますので、よろしければご覧ください。

qiita.com

Controlled なフォーム要素は以下のルールがあります。

  • type 属性が radiocheckbox の input 要素の場合は checked prop を指定する
  • それ以外のフォーム要素は value prop を指定する
const ControlledInput = () => {
  const [value, setValue] = useState('');
  return <input type="text" onChange={e => setValue(e.target.value)} value={value} />;
}

対照的に、初期値を持つ Uncontrolled なフォーム要素は以下のルールがあります。

  • type 属性が radiocheckbox の input 要素の場合は defaultChecked prop を指定する
  • それ以外のフォーム要素は defaultValue prop を指定する
const UncontrolledInput = () => <input type="text" defaultValue="test" />;

フォーム要素は Controlled または Uncontrolled のいずれかである必要があるため、valuedefaultValue、または checkeddefaultChecked props 両方を含めることはできません。

const MixControlledWithUncontrolledInput = () => {
  const [value, setValue] = useState('');
  return <input type="text" onChange={e => setValue(e.target.value)} value={value} defaultValue="test" />;
}

上のコードのようにフォーム要素に valuedefaultValue props 両方指定すると React はコンソール上で警告メッセージを出力します。

Warning: App contains an input of type text with both value and defaultValue props. Input elements must be either controlled or uncontrolled (specify either the value prop, or the defaultValue prop, but not both). Decide between using a controlled or uncontrolled input element and remove one of these props. More info: https://reactjs.org/link/controlled-components

これをチェックできるプラグインを作成した

eslint-plugin-react-form-fields はこれらの React のフォーム固有のルールを ESLint である程度チェックできるようにするためのプラグインです。

github.com

ルール 説明 推奨
react-form-fields/no-mix-controlled-with-uncontrolled フォーム要素にvaluedefaultValue、または checkeddefaultChecked props 両方指定することを禁止 ⭐️
react-form-fields/no-only-value-prop フォーム要素に onChangereadOnly props を無しに valuechecked のみを指定することを禁止 ⭐️
react-form-fields/styled-no-mix-controlled-with-uncontrolled styled-components に react-form-fields/no-mix-controlled-with-uncontrolled のルールを適用
react-form-fields/styled-no-only-value-prop styled-components に react-form-fields/no-only-value-prop のルールを適用

先に、React のフォーム要素固有のルールに反すると React はコンソール上で警告メッセージを出力すると説明しました。 それにもかかわらず、この ESLint プラグインを作成する必要があるか疑問に思う方も多いと思います 🤔

これについては次のセクションで説明します。

ESLint プラグインの作成経緯

あるメンバーが input 要素に valuedefaultValue props 両方指定したことによりコンソール上で警告メッセージが出力されてしまい、それを別のメンバーが気付いて修正を行ったことがありました。

フロントエンドメンバーでの MTG でこれに関する話題が上がり、

  • コンソール上の確認が漏れてしまったり、React のフォーム実装に熟知していないメンバー(初めてフォームを実装する人など)でも気づけるような仕組みは作れないか?
  • ESLint でこれを防ぐためのプラグインありそうでは?

という話になりました。

しかし、これを防ぐための ESLint プラグインは探してみましたが存在しないようです…。

f:id:kotarella1110:20210524100737p:plain:w512

これは作るしかない…!ということで以下の理由から ESLint プラグインの作成することにしました。

  • ルール(input 要素に valuedefaultValue props 両方指定されていたら検知する)が複雑ではないため難しくなさそう
  • ESLint プラグインを作成したことなかったのでいい経験になりそう
  • eslint-plugin-react のコードを眺めていたところ、これはいけるなーという感覚になった

f:id:kotarella1110:20210524100741p:plain:w512

ESLint プラグイン実装の話

似た ESLint プラグインのコードを読み、作成方法についてキャッチアップ

先に簡単に触れましたが、eslint-plugin-react の button-has-type というルールのコードを眺めていたところ、これはいけるなーという感覚になりました。 というのも、button-has-type と作成したいプラグインのルールに違いはあれど、「JSXElement に渡した JSXAttribute を取得して何かしらを検知する」ものであり両者のロジック自体は似通っていたためです。また、button-has-type で使用されている jsx-ast-utils という JSX を静的解析するための AST ユーティリティを使用すれば、今回作成したいプラグインを簡単に実装できそうということが分かりました。これを使用することで簡単に JSXElement のタグ名を取得したり、JSXElement に渡した JSXAttribute やJSXAttribute の値の取得などができます。

また、button-has-type のコードリーディングと並行して、ESLint 公式ドキュメントや有用な記事を読んだりして ESLint プラグインの作り方についての理解も深めていきました。

eslint.org qiita.com techblog.yahoo.co.jp

AST についてキャッチアップ

なんとなく ESLint プラグインの作成方法について理解できましたが、当時「AST 全くワカラン」状態だったため、以下の記事を読んで AST をざっくりと理解しました。 ESLint は、コードをパースしてできた AST を ESLint のルール(プラグイン)で検証し、エラーや警告を出力するため、ESLint プラグインを作成する上で、AST に関する理解も必須です。

efcl.info

更に理解を深めるために、実際のコードがどのような AST にパースされるかを AST Explor で確認して遊んだりしました。

astexplorer.net

後は実装するのみです!💪

ESLint プラグインの作成過程をライブコーディング

嬉しいことに、READYFOR の要素セレクタ警察こと @neripark さんに「ESLint プラグインを実装するライブコーディングしてほしい」と依頼を受けました!

f:id:kotarella1110:20210524100745p:plain:w512

よっしゃ楽しそうだしやるかーという感じでラフにライブコーディングを開催しました。

f:id:kotarella1110:20210521004712p:plain:w512

参加者とワイワイしながらある程度のものがこの場で完成しました 🎉

f:id:kotarella1110:20210521153554p:plain:w512

ESLint プラグインテンプレートの作成

ある程度のものがライブコーディングで完成してはいたのですが、いい感じのリポジトリにしたかったので ESLint プラグインテンプレートを探していたところ、素晴らしい記事が…!

qiita.com

@mysticatea さんの template-eslint-plugin をそのまま使おうとしたのですが、ESLint プラグインを TypeScript で記述したかったため、こちらのテンプレートをベースに ESLint プラグインテンプレートを作成しました。(@mysticatea さんにはこの場を借りて感謝申し上げます 🙇‍♂️)

github.com

※ TypeScript で記述するために @typescript-eslint/experimental-utils を使用しています。

github.com

ESLint プラグインの公開

作成したテンプレートをベースに実装が完了し、以下の二つのルールを公開することができました!

f:id:kotarella1110:20210521171408p:plain:w512

とはいえ…この時点ではプラグインが使い物になりませんでした… 😂

これは、READYFOR ではスタイリングに @emotion/styled を採用しているためです(記法自体は styled-components とほとんど変わりません)。 そのため、styled-components をサポートする必要がありました。

styled-components のサポート

styled-components をサポートする方法を調べていたところ以下の ESLint プラグインを発見しました。 このプラグインは、styled-components に eslint-plugin-jsx-a11y のルールを適用します。

github.com

eslint-plugin-react-form-fields もこのプラグインを参考にして styled-components をサポートするために実装を進めました。 コードリーディングを行い全体のロジックをある程度理解するまで多少時間がかかり大変でしたが、以下の二つのルールを追加し、無事サポートをすることができました!

f:id:kotarella1110:20210521171534p:plain:w512

styled-components の .attrs"as" polymorphic propextend を使用しても正しく機能します。

extend:

let StyledInput = styled.input.attrs({
  value: 'test',
})``;
let ExtendedInput = styled(StyledInput).attrs({
  defaultValue: 'test',
})``;
let Hello = <ExtendedInput />; // エラー

.attrs:

let StyledInput = styled.input``;
let Hello = <StyledInput value="test" defaultValue="test" />;

let StyledInput = styled.input.attrs({
  value: 'test',
})``;
let Hello = <StyledInput defaultValue="test" />; // エラー

"as" polymorphic prop:

let StyledDiv = styled.div``;
let Hello = <StyledDiv as="input" value="test" defaultValue="test" />; // エラー

もし、ESLint プラグインで styled-components をサポートしたい!という方がいればこちらの PR をご参考にしていただければと思います 🙏

今後の展望

任意のフォームコンポーネントもチェックの対象になるように改善する

現状、この ESLint プラグインでエラー検知できるのはフォーム要素のみです。 以下のように、フォーム要素を内包したコンポーネントのエラー検知はできません。

const InputText = (props) => {
  return <input {...props} type="text" />;
};

const Form = () => (
  <form>
    <InputText value="test" defaultValue="test" /> {/* ESLint でエラーを検知したい */}
    <input type="submit" />
  </form>
);

そのため、components といったオプションを提供し、 そこに指定されたコンポーネントをエラー検知できるように改善することを検討しています。

.eslintrc.js:

  "rules": {
    "react-form-fields/no-mix-controlled-with-uncontrolled": ["error", { "components": [ "InputText" ] }],
    "react-form-fields/no-only-value-prop": ["error", { "components": [ "InputText" ] }]
  }

jsx-ast-utils の型定義ファイル作成

jsx-ast-utils は JS で記述されており、型定義ファイルは現状存在しません。 DefinitelyTyped にも存在しないようです。 eslint-plugin-react-form-fields は TS で記述されているということもあるので、jsx-ast-utils の型定義ファイルを作成してコントリビュートしていきたいです。 (我こそは!という方がいれば是非コントリビュートお願いしたいです!)

styled-components の AST ユーティリティ作成

jsx-ast-utils は元々 eslint-plugin-jsx-a11y のコードに含まれていましたが、それ抽出して別で維持することが有用だろうという判断から生まれたようです。 自分も同じように、eslint-plugin-react-form-fields のコードに含まれている styled-components の AST のロジックを抽出してユーティリティを作成することで、誰かの役に立てれれば嬉しいです。

最後に

ESLint プラグインの作成自体言語処理系ということもあり難しいイメージがありましたが、 作りたい ESLint のルールが複雑でなければ、割と簡単に ESLint プラグインを作成できます。 また、ESLint プラグインを作成過程で AST をゴニョるの楽しくなってきます! 今回得た知見は Babel プラグインの作成等にも生かすことができそうです。 興味ある方は是非 ESLint プラグインを作ってみることをお勧めします!

それでは 👋