🐬

[React]コーディング規約を考えてみた[Typescript]

2023/11/29に公開
2

本記事のコーディング規約は ESLint で設定したルールの説明書に近い位置付けです。
ルール設定出来ない規約を書いてもいつか忘れるからです 🐢

こちらで本記事の eslint を設定しています 🥳🥳
https://zenn.dev/tara_is_ok/articles/271aebe29f921e

💀 爆弾系

react-hooks の依存配列

より良い例募集中です!🙇‍♂️

bad🥶

useEffect(() => {
  // ...
  // eslint-ignore-next-line react-hooks/exhaustive-deps
}, []);

good🤗

useEffect(() => {
  // ...
}, [state]);

useEffect

より良い例募集中です!

bad🥶

const [firstName, setFirstName] = useState("Taylor");
const [lastName, setLastName] = useState("Swift");

// 🔴 不要な状態
const [fullName, setFullName] = useState("");

useEffect(() => {
  setFullName(firstName + " " + lastName);
}, [firstName, lastName]);

good🤗

const [firstName, setFirstName] = useState("Taylor");
const [lastName, setLastName] = useState("Swift");
// ✅ レンダリングごとに計算する
const fullName = firstName + " " + lastName;

bad🥶

const { data } = useSWR(key, fetcher);
const [response, setResponse] = useState(); //apiのresponse

// 🔴 dataとresponseは2重管理
useEffect(() => {
  if (!data) return;
  setResponse(data.response);
}, [data, setResponse]);

good🤗

// ✅ data.responseで良い
const { data } = useSWR(key, fetcher);

eslint-disable を使う場合はコメントを残す

  • なぜ disable にする必要があったかを明記する

bad🥶

// eslint-disable--next-line react-hooks/exhaustive-deps

// eslint-disable--next-line react-hooks/exhaustive-deps -- 必要だった 🙅‍♀️

good🤗

// eslint-disable--next-line react-hooks/exhaustive-deps -- コメント(なぜを書く🙆)

マジックナンバー

  • 使わない。
  • ただし配列検索に0, 1, -1あたりは使うので許容する (indexOf() === -1 など)
  • data[100]は許容するが、定数で切り出した方が優しい
  • http の status はギリギリ許容するがhttp-statusなどのライブラリで定義されている定数を使うことを推奨する

    理由: 可読性 🙅‍♀️

参考
bad🥶

switch (code) {
    case 13:
      return '何かの処理1'
    case 11:
      return '何かの処理2'
}

switch(status){
  case 401: //何か
  case 403: //何か
  ...
}

//マジックナンバー解消例🫥
const code13 = 13
const code11 = 11

good🤗


switch (code) {
    case tokyo:
      return '何かの処理1'
    case saitama:
      return '何かの処理2'
}

switch(status){
  case UNAUTHORIZED: //何か
  case FORBIDDEN: //何か
  ...
}

//意味を持たせる🧖‍♂️
const tokyo = 13
const saitama = 11

any

  • 使わない

    理由: typescript を使っているメリットがなくなります

cast

  • 使わない

    理由: typescript を使っているメリットがなくなります

bad🥶

type User = { id: number; name: string };
const user = { name: "you" } as User;

good🤗

type User = { id: number; name: string };
const user = { id: 1, name: "you" };

null vs undefined

  • 明示的に使用不可能にするために、どちらも使用しないことを推奨します。

    理由: これらの値は、値間の一貫した構造を維持するためによく使用されます。TypeScript では型を使用して構造を表します

  • ||ではなく??を使用する
    • Null 合体演算子

      理由: undefined と null 以外の falsy な値(0 や"")を許容したい時に||を使う場合右辺値となる

bad🥶

let foo = { x: 123, y: undefined };
const value1 = ""; //または0
const value2 = "default value";
const result = value1 || value2;
//result: value2

good🤗

let foo: { x: number; y?: number } = { x: 123 };
const value1 = ""; //または0
const value2 = "default value";
const result = value1 ?? value2;
//result: value1
  • 一般的に undefined を使用してください(代わりに{valid:boolean,value?:Foo}のようなオブジェクトを返すことを検討してください)
  • jsx で null を返す必要がある際は、<></>を使う

bad🥶

return null;

good🤗

return undefined;
or;
return <></>;
  • API または従来の API の一部である場合は null を使用します

    理由: Node.js の慣例通りです。NodeBack スタイルコールバックの error は null です。

bad🥶

cb(undefined);

good🤗

cb(null);

🧘‍♀️ マインド

1 ファイル(1 コンポーネント)100 行以内を意識する

  • 100 行以上書かない
    • 100 行以上のファイルが誕生しそうな場合は、ロジックやコンポーネントを自分の中で分解できていない可能性大
    • 傾向としてその場凌ぎの修正が多くあるファイルは条件分岐が増えがち
      • 根本的な解決を意識する!
    • 1 ファイルで複数機能を持つpagesとかutilsとかtestなどは許容する

      理由: 共通化の意識が高まる。単純に読むの大変

共通化は必ずしも正義ではない

  • 小さく作る。その恩恵の 1 つに共通化がある。
  • 条件や props が増えそうだと予想されるものは無理に共通化しない
  • 関数やコンポーネントで同じ処理をしているからといって切り出さない
    • たまたま上手く共通化出来ただけ
  • UI などの共通化は、そもそものデザインが原因で不要な条件が発生する場合があるのでデザイナーと相談する
  • 再利用目的で小さく作るわけではないも読むと 👏

理由: 手段が目的となっています。かえって読みづらいです。共通化を試みすぎて自分達の首が締まったことがあるはず、、、

純粋な関数を心がける

  • 副作用は含めずに関数を分ける
    • 1 つの大きな関数でuseStateを多用しない
    • api request を行う関数の中で useState などの副作用は行わない
      • 「引数を受け取って api response を返す」のみ ✅
  • 引数は後から増やさないことを意識する

    理由: テストが容易となります

bad🥶

const [data, setData] = useState()
const request = async (formData) => {
const body = { ~~ } //データ整形
const {data} =  await axios.request({
  url,
  method:'get',
  data: body
})
setData(data)
}

good🤗

//api request用にデータを整形する関数
const toBody = (formData) => {return ~~ }

//api responseのみを返す関数
const fetchData = async (formData) => {
const body = toBody(formData)
try{
  const response = await axios.request({
    url,
    method:'get',
    data: body
  return response
  })catch(error){
  console.error(error)
  }
}
const { data } = await fetchData(formData);
setData(data);

UI コンポーネント用などにデータを整形しない

  • UI 側(使う)時に整形する
    • データを表示側の責務
  • UI 側に合わせるために無理に関数化しないくて良い
  • 整形した Data を map を回すのではなくコンポーネントを愚直に書いていく

    理由: 条件が肥大化しがち。ロジック側の責務ではないです。

bad🥶

//api requestを行っているファイル
const {data} = useSWR(key,fetcher)

const toRows = (data, 他色々な条件) => {
// ifやswitchなどの条件たくさん
}

//api requestに関係のない関数
return { data, toRows}

//使う時
//各Cellの表示で条件が増えた時に辛い🔴
<UserTable rows={toRows(data)} />

good🤗

//api requestを行っているファイル
const {data} = useSWR(key,fetcher)
//dataのみを返す✅
return {data}

//使う時
<UserTable data={data} />

//UserTable.tsx
<TableContainer>
  <Table>
    <TableHead>
      <TableRow>
        <TableCell>id</TableCell>
        <TableCell>name</TableCell>
          </TableRow>
    </TableHead>
    <TableBody>
      {data.map((row) => (
      //共通化は意識せずに愚直にかく✅
        <TableRow key={row.id}>
          <TableCell>{row.id}</TableCell>
          <TableCell{row.name}</TableCell>
        </TableRow>
      ))}
    </TableBody>
  </Table>
</TableContainer>

メモ化をするにコードを見直す

  • 本当に必要か考える
    • 脳死でuseMemo, useCallbackを使えばパフォーマンスが上がるわけではない

🫥 コードスタイル

変数と関数

  • 変数と関数名には camelCase を使います

    理由: 従来の javascript

  • 名前は基本省略しない
    • map で回す要素などは許容する

      理由: 誰が見ても分かるようにすべきです。文字数の多さで省略は判断しない

  • 配列は{名詞} + sとする
    • count ⇔ counts

      理由: list はプログラムにおいて特別な意味がある。より直感的

bad🥶

let FooVar;
const BarFunc = () => {};
const cnts = [1, 2, 3, 4, 5]; //またはcountList

good🤗

let fooVar;
const barFunc = () => {};
const counts = [1, 2, 3, 4, 5];
counts.map((count, key) => {});

三項演算子

  • ネストしない

    理由: 可読性 😰

bad🥶

let x = 10; // x = 3
let result = x > 0 ? (x % 2 === 0 ? "Positive Even" : "Positive Odd") : (x < 0 ? "Negative" : "Zero");
console.log(result);
→ 何が出力される?😨🤔

good🤗

let x = 10; // x = 3
let result;

switch (true) {
  case x > 0:
    result = x % 2 === 0 ? "Positive Even" : "Positive Odd";
    break;
  case x < 0:
    result = "Negative";
    break;
  default:
    result = "Zero";
}
  • ??が使えるか確認する

    理由: 冗長

bad🥶

const result = value ? value : defaultValue;

good🤗

const result = value ?? defaultValue;

型推論

  • 推論が効いている場合は指定しない

    理由: 冗長

bad🥶

const name: string = "foo";
const [count, setCount] = useState<number>(0);
const [data, setData] = useState<Data>(defaultData); //defaultData: Data型

good🤗

const name = "foo";
const [count, setCount] = useState(0);
const [data, setData] = useState(defaultData);

配列型

  • 配列に foos: Array<Foo>の代わりに foos: Foo[]として配列にアノテーションをつけます。

    理由: 読みやすい。TypeScript チームによって使用されています。脳が[]を検出するように訓練されているので、何かが配列であることを知りやすくなります。

  • 複数形の配列は作らない

    理由: 冗長。

bad🥶

type Count = number;
type Counts = Array<Count>;
type CountList = Count[];

type Props = {
  counts: Counts; //countList: countList
};

good🤗

type Count = number;

type Props = {
  counts: Count[];
};

ファイル名

  • camelCase を使って全てのファイル / フォルダに名前を付けます。例えばaccordion.tsxmyControl.tsxutils.tsmap.tsなどです。

    理由: 多くの JS チームで慣習的です。特定のファイル/フォルダのみ大文字にするならば基準(ルール)を作る。

bad🥶

/SomeComponent.tsx

good🤗

/someComponent.tsx

type vs interface

  • 基本 type を使う

    理由: インターフェースは意図せず定義のマージや上書きが起こることがあります

bad🥶

interface Foo {}

interface Props extends UiLibraryProps {
  yourAttribute: string;
}

good🤗

type Foo = {};

type Props = UiLibraryProps & {
  yourAttribute: string;
};

default export vs named export

  • 基本的にはnamed exportを使用する
    • next.js のファイルルーティングは例外

      理由: default export の場合、ファイル名を変更したときに検知できない(参考)

bad🥶

export default Foo;
import Foo from "./foo";

good🤗

export const Foo = () => {};
import { Foo } from "./foo";

オブジェクトの省略記法

bad🥶

const user = {
  id: id,
  name: "foo",
};

good🤗

const user = {
  id,
  name: "foo",
};

boolean 型の属性

  • 省略可能

bad🥶

<Component personal={true} />

good🤗

<Component personal />

Enum

  • enum 名にはPascalCaseを使います

    理由: クラスと同様

  • enum メンバに PascalCase を使用する

    理由: 言語作成者、TypeScript チームに従った慣例です。例えば SyntaxKind.StringLiteral です。他の言語から TypeScript への翻訳(コード生成)にも役立ちます。

bad🥶

enum color {
  red,
}

good🤗

enum Color {
  Red,
}

Interface

  • 基本的には使わない
  • 名前にはPascalCaseを使います。

    理由: クラスと同様

  • メンバには camelCase を使います。

    理由: クラスと同様

  • プレフィックスに I をつけないでください

    理由: 慣例的ではない。

bad🥶

interface IFoo {
  Baz: string;
}

good🤗

interface Foo {
  baz: string;
}

クラス

  • クラス名にはPascalCaseを使います。

    理由:これは実際には標準の JavaScript ではかなり一般的です。

  • クラスメンバとメソッドのcamelCaseを使う

    理由: 変数と関数の命名規則に従います

bad🥶

class foo {
  Bar: number;
  Baz() {}
}

good🤗

class Foo {
  bar: number;
  baz() {}
}

🖇️ テンプレート

ルール名

  • ルール詳細

    理由:

bad🥶

bad code

good🤗

good code

📘 参考

🪺 こちらも!

このコーディング規約を反映させた環境構築の記事を書きました 🌱
https://zenn.dev/tara_is_ok/articles/05b3a6dc2ebdd7

Discussion

Honey32Honey32

共通化は必ずしも正義ではない

これは同意です。共通化に囚われると条件分岐が増えて把握しきれなくなるので、低機能で小さな部品の組み合わせにして、それとは別に「変わることがない小さな概念のかたまり」を見つけたときだけ、その部品を共通のものとしたいですね。

スクラップですが、このような記事があるので、参考にどうぞ!

https://scrapbox.io/mrsekut-p/再利用目的で小さく作るわけではない

tara is oktara is ok

コメントありがとうございます!🥹

低機能で小さな部品の組み合わせにして、それとは別に「変わることがない小さな概念のかたまり」を見つけたときだけ、その部品を共通のものとしたいですね。

こちらおっしゃる通りで私のモヤモヤが解消された気がします。記事にも追記させていただきました✨
添付いただいた記事も読みたいと思います💪

過去にモーダルの共通化を試みて要件やデザインの変更に耐えれずに時限爆弾と化したソースを見たことをきっかけに共通化とは?を考えるようになりました🤔