📝

チームで同じような React コンポーネントを書く

2024/02/11に公開
1

はじめに

eslint-plugin-reactplugin: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-reactreact/react-in-jsx-scope を無効化するための設定です。React のバージョン 17 よりも上のバージョンを使用している場合は import React from 'react' が不要になります。しかし、これらのルールが有効の場合、このインポート文を書かないとエラーになるため、recommended で有効化したものを、jsx-runtime で無効化しています。extends は同じルールがあった場合、後に書かれたものが優先されるため、順番には注意が必要です。

import React from 'react' が不要になった経緯など、詳しい解説は uhyo さんの記事が参考になります。

https://zenn.dev/uhyo/articles/react17-new-jsx-transform

(宣伝) もし ESLint の設定ファイルの読み方が分からない方は、以前に書いた記事がありますので、参考にしてみてください。

https://zenn.dev/kazukix/articles/eslint-config-react-native

ルールの説明

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>;
};
GitHubで編集を提案

Discussion

渡波 空渡波 空

最後の

const Button = ({ onClick }: { onClick: () => void }) => {
  const handleClick = onClick;
  return <button onClick={handleClick}>送信</button>;
};

const Button = ({ onClick: handleClick }: { onClick: () => void }) => {
  return <button onClick={handleClick}>送信</button>;
};

とすれば引数部分で名前を変更できますね(どっちにせよまどろっこしいけど)