チームで同じような React コンポーネントを書く
はじめに
「eslint-plugin-react の plugin:react/recommended
に含まれていないルールにも有効なものがあるよ!みんなで同じようなコンポーネント書いて、レビューを楽にして保守性も上げよう!」という内容の記事です。
この記事では React の関数コンポーネント、TypeScript、Prettier を使っている前提で書いています。そのため、タグの位置調整など Prettier で対応可能なものは Prettier に任せる方針です。
先に結論の .eslintrc.cjs
を載せておきます。React 以外の設定は省いています。
{
extends: [
"plugin:react/recommended",
"plugin:react/jsx-runtime",
"plugin:react-hooks/recommended"
],
rules: {
"react/destructuring-assignment": "error",
"react/function-component-definition": [
"error",
{
namedComponents: "arrow-function",
unnamedComponents: "arrow-function",
},
],
"react/hook-use-state": "error",
"react/jsx-boolean-value": "error",
"react/jsx-fragments": "error",
"react/jsx-curly-brace-presence": "error",
"react/jsx-no-useless-fragment": "error",
"react/jsx-sort-props": "error",
"react/self-closing-comp": "error",
"react/jsx-pascal-case": "error",
"react/no-danger": "error",
}
}
plugin:react/jsx-runtime
は初めてみた方もいるかもしれません。これは plugin:react/recommended
により有効化された ESLint ルールである react/jsx-uses-react
と react/react-in-jsx-scope
を無効化するための設定です。React のバージョン 17 よりも上のバージョンを使用している場合は import React from 'react'
が不要になります。しかし、これらのルールが有効の場合、このインポート文を書かないとエラーになるため、recommended
で有効化したものを、jsx-runtime
で無効化しています。extends
は同じルールがあった場合、後に書かれたものが優先されるため、順番には注意が必要です。
import React from 'react'
が不要になった経緯など、詳しい解説は uhyo さんの記事が参考になります。
(宣伝) もし ESLint の設定ファイルの読み方が分からない方は、以前に書いた記事がありますので、参考にしてみてください。
ルールの説明
plugin:react/recommended
に含まれているルールについては記載しません。
destructuring-assignment
Props などの分割代入を強制。
// Bad
const MyComponent = (props) => {
return <div id={props.id} />;
};
// Good
const MyComponent = ({ id }) => {
return <div id={id} />;
};
const MyComponent = (props) => {
const { id } = props;
return <div id={id} />;
};
function-component-definition
関数コンポーネントの定義方法を統一。関数宣言、関数式、アロー関数から選べます。
念の為、関数宣言、関数式、アロー関数の復習です。
// 関数宣言
function MyComponent() {
return <div />;
}
// 関数式
const MyComponent = function () {
return <div />;
};
// アロー関数
const MyComponent = () => {
return <div />;
};
hook-use-state
useState
の返り値の命名を [value, setValue]
に統一。
// Bad
const [count, updateCount] = useState(0);
// Good
const [count, setCount] = useState(0);
jsx-boolean-value
boolean 型の Props の渡し方を統一。
// Bad
<Checkbox checked={true} />
// Good
<Checkbox checked />
// false は省略できません
<Checkbox checked={false} />
jsx-fragments
React Fragment の書き方を統一。
// Bad
<React.Fragment><Foo /></React.Fragment>
// Good
<><Foo /></>
<React.Fragment key="key"><Foo /></React.Fragment>
jsx-curly-brace-presence
Props と children で不要な中括弧を削除する。
// Bad
<Button type={"button"}>{"送信する"}</Button>
// Good
<Button type="button">送信する</Button>
jsx-no-useless-fragment
不要な React Fragment を削除する。
// Bad
<>{foo}</>
<><Foo /></>
// Good
{foo}
<Foo />
jsx-sort-props
Props の並び順をアルファベット順にする。
// Bad
<Hello lastName='Smith' firstName='John' />;
// Good
<Hello firstName='John' lastName='Smith' />;
self-closing-comp
子要素がない場合は自己終了タグを使う。
// Bad
<Foo></Foo>
// Good
<Foo />
jsx-pascal-case
コンポーネント名をパスカルケースにする。
// Bad
<Test_component />
// Good
<TestComponent />
no-danger
dangerouslySetInnerHTML
を許可しない。使いたい場合は eslint-disable react/no-danger
+ 説明コメントを書くくらいが良いと思っています。
// Bad
<div dangerouslySetInnerHTML={{ __html: 'Hello World' }} />
余談ですが、dangerouslySetInnerHTML
は内部で innerHTML
を使用しており、innerHTML は XSS 対策として、挿入された script タグを実行しないため、このようなスクリプトは実行されません。
<div
dangerouslySetInnerHTML={{
__html: `<script>alert('hello world!')</script>`,
}}
/>
しかし、img タグの onerror
属性などは実行されるため、下のスクリプトは実行されます。安易に dangerouslySetInnerHTML
を使わないようにしましょう。
<div
dangerouslySetInnerHTML={{
__html: `<img src="invalid" onerror="alert('hello world!')" />`,
}}
/>
(番外編) ちょっと過激なルール
チームによっては有効にしても良さそうな、少し過激めのルールも紹介します。
boolean-prop-naming
boolean 型の Props の命名を統一。デフォルトでは is
または has
で始まる名前にする必要があります。(ただ、2024 年 2 月現在こちらのルール手元で試してみたところ動いていなそうでした。Issue にもなっていて、現在は使用できない可能性がありそうです。)
// Bad
<Checkbox checked />
// Good
<Checkbox isChecked />
jsx-handler-names
イベントハンドラの命名を統一。デフォルトでは、ハンドラ関数は handle
で始まり、ハンドラ関数を受け取る Props は on
で始まる名前にする必要があります。
// Bad
<Checkbox handleChange={handleChange} />
<Checkbox onChange={onChange} />
// Good
<Checkbox onCheck={handleCheck} />
ただ、下のコードもエラーになってしまうため、ちょっと使いにくいかもしれません。
const Button = ({ onClick }: { onClick: () => void }) => {
// on から始まるハンドラ関数を onClick に渡そうとしてエラー
return <button onClick={onClick}>送信</button>;
};
// 流石にこんなことはしたくない...
const Button = ({ onClick }: { onClick: () => void }) => {
const handleClick = onClick;
return <button onClick={handleClick}>送信</button>;
};
Discussion
最後の
は
とすれば引数部分で名前を変更できますね(どっちにせよまどろっこしいけど)