こんにちは。READYFOR でフロントエンドエンジニアとして働いている菅原(@kotarella1110)です! 最近 React のフォームライブラリとして React Hook Form を社内で採用することになり、コントリビュートしている身としてはかなりテンションが上がっております 🙌
話は変わりますが、先日 eslint-plugin-react-form-fields という ESLint プラグインを公開しました 🎉
published 🎉
— Kotaro (@kotarella1110) 2021年4月6日
kotarella1110/eslint-plugin-react-form-fields: React Form Fields specific linting rules for ESLint https://t.co/ghBILvIle7
ESLint プラグインを作ること自体初めてだったのですが、思ったより簡単に ESLint プラグインを作成できました。 そこで、本記事ではこの ESLint プラグインの作成経緯やプラグインの実装の話などについて記載します。
今回作成した ESLint プラグインについて
React のフォーム固有のルールがいくつか存在
React のフォームには固有のルールがいくつか存在します。 その中でも代表的なルールを一つ挙げると、React のフォーム要素は Controlled または Uncontrolled のいずれかである必要があります。 Controlled Components は React によって入力値を制御しますが、Uncontrolled Components は入力値を DOM 自身(ブラウザ)が制御します。 両者の比較や違いについてこちらに詳しく記載しておりますので、よろしければご覧ください。
Controlled なフォーム要素は以下のルールがあります。
type
属性がradio
かcheckbox
の 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
属性がradio
かcheckbox
の input 要素の場合はdefaultChecked
prop を指定する- それ以外のフォーム要素は
defaultValue
prop を指定する
const UncontrolledInput = () => <input type="text" defaultValue="test" />;
フォーム要素は Controlled または Uncontrolled のいずれかである必要があるため、value
と defaultValue
、または checked
と defaultChecked
props 両方を含めることはできません。
const MixControlledWithUncontrolledInput = () => { const [value, setValue] = useState(''); return <input type="text" onChange={e => setValue(e.target.value)} value={value} defaultValue="test" />; }
上のコードのようにフォーム要素に value
と defaultValue
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 である程度チェックできるようにするためのプラグインです。
ルール | 説明 | 推奨 |
---|---|---|
react-form-fields/no-mix-controlled-with-uncontrolled | フォーム要素にvalue と defaultValue 、または checked と defaultChecked props 両方指定することを禁止 |
⭐️ |
react-form-fields/no-only-value-prop | フォーム要素に onChange と readOnly props を無しに value と checked のみを指定することを禁止 |
⭐️ |
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 要素に value
と defaultValue
props 両方指定したことによりコンソール上で警告メッセージが出力されてしまい、それを別のメンバーが気付いて修正を行ったことがありました。
フロントエンドメンバーでの MTG でこれに関する話題が上がり、
- コンソール上の確認が漏れてしまったり、React のフォーム実装に熟知していないメンバー(初めてフォームを実装する人など)でも気づけるような仕組みは作れないか?
- ESLint でこれを防ぐためのプラグインありそうでは?
という話になりました。
しかし、これを防ぐための ESLint プラグインは探してみましたが存在しないようです…。
これは作るしかない…!ということで以下の理由から ESLint プラグインの作成することにしました。
- ルール(input 要素に
value
とdefaultValue
props 両方指定されていたら検知する)が複雑ではないため難しくなさそう - ESLint プラグインを作成したことなかったのでいい経験になりそう
- eslint-plugin-react のコードを眺めていたところ、これはいけるなーという感覚になった
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 に関する理解も必須です。
更に理解を深めるために、実際のコードがどのような AST にパースされるかを AST Explor で確認して遊んだりしました。
後は実装するのみです!💪
ESLint プラグインの作成過程をライブコーディング
嬉しいことに、READYFOR の要素セレクタ警察こと @neripark さんに「ESLint プラグインを実装するライブコーディングしてほしい」と依頼を受けました!
よっしゃ楽しそうだしやるかーという感じでラフにライブコーディングを開催しました。
参加者とワイワイしながらある程度のものがこの場で完成しました 🎉
ESLint プラグインテンプレートの作成
ある程度のものがライブコーディングで完成してはいたのですが、いい感じのリポジトリにしたかったので ESLint プラグインテンプレートを探していたところ、素晴らしい記事が…!
@mysticatea さんの template-eslint-plugin をそのまま使おうとしたのですが、ESLint プラグインを TypeScript で記述したかったため、こちらのテンプレートをベースに ESLint プラグインテンプレートを作成しました。(@mysticatea さんにはこの場を借りて感謝申し上げます 🙇♂️)
※ TypeScript で記述するために @typescript-eslint/experimental-utils を使用しています。
ESLint プラグインの公開
作成したテンプレートをベースに実装が完了し、以下の二つのルールを公開することができました!
いい学びになった。https://t.co/ghBILvIle7 pic.twitter.com/N744hF7MfS
— Kotaro (@kotarella1110) 2021年4月6日
とはいえ…この時点ではプラグインが使い物になりませんでした… 😂
これは、READYFOR ではスタイリングに @emotion/styled を採用しているためです(記法自体は styled-components とほとんど変わりません)。 そのため、styled-components をサポートする必要がありました。
styled-components のサポート
styled-components をサポートする方法を調べていたところ以下の ESLint プラグインを発見しました。 このプラグインは、styled-components に eslint-plugin-jsx-a11y のルールを適用します。
eslint-plugin-react-form-fields もこのプラグインを参考にして styled-components をサポートするために実装を進めました。 コードリーディングを行い全体のロジックをある程度理解するまで多少時間がかかり大変でしたが、以下の二つのルールを追加し、無事サポートをすることができました!
- react-form-fields/styled-no-mix-controlled-with-uncontrolled
- react-form-fields/styled-no-only-value-prop
styled-components の .attrs
や "as"
polymorphic prop、extend を使用しても正しく機能します。
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 をご参考にしていただければと思います 🙏
styled-components をサポートした 🎉
— Kotaro (@kotarella1110) 2021年5月13日
いい学びになった。
feat: support styled-components by kotarella1110 · Pull Request #45 · kotarella1110/eslint-plugin-react-form-fields https://t.co/HpIX73MQAJ
今後の展望
任意のフォームコンポーネントもチェックの対象になるように改善する
現状、この 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 プラグインを作ってみることをお勧めします!
それでは 👋