🐬

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

2023/11/29に公開
2

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

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

💀 爆弾系

react-hooksの依存配列

  • 無視しない
  • useEffectで依存配列を正しく書いた時に無限レンダーなどが起きる場合useEffect内のロジックが間違っている可能性大

理由: react-hooks/exhaustive-deps無視してない?

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

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では型を使用して構造を表します

理由: 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

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

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

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

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