はじめに
個人開発で迷うポイントを減らすための、自分用コーディングルール。
他の記事やChatGPTの回答を参考にしつつ、まだ整理できていない部分もある。
開発を進める中で必要に応じて内容を見直し、継続的にブラッシュアップしていく。
変数・定数・値の扱い
let,varは原則使わない
理由
・値の再代入を防ぎ、コードの意図が明確になる
・「途中で値が変わるかもしれない」という読み手の不安を減らす
・予期しない動作が起きづらくなる
- 変数の宣言にはconstを使用する
- 値が本当に変わる必要がある場合のみletを検討する
ハードコーディング/マジックナンバー禁止
理由
・意味が不明瞭でコードの可読性を下げる
・意図が変わった場合に修正箇所を特定しづらく、バグの温床になる
・同じ数字を別の意味で再利用すると混乱の原因になる
- 数値は定数やenumに名前を付けて管理する
bad
// ボタンの横幅を 200px に固定している → 数字の意味が不明
const MyButton = () => {
return <button style={{ width: 200 }}>Click me</button>;
};
// ロジック内で 7 を使っている → 何の意味の 7 かわからない
const isLucky = (num: number) => {
return num % 7 === 0;
};
good
// 定数に名前をつけて意味を明確に
const BUTTON_WIDTH_PX = 200;
const MyButton = () => {
return <button style={{ width: BUTTON_WIDTH_PX }}>Click me</button>;
};
// マジックナンバーを定数化して意図を明確に
const LUCKY_NUMBER = 7;
const isLucky = (num: number) => {
return num % LUCKY_NUMBER === 0;
};
型安全
anyは使用禁止
理由
・TypeScriptの型チェックが効かず、実質JavaScriptと同じになる
・実行しないと失敗が分からないため、保守性が低下する
- 必要な型は必ず自分で定義する
- どうしても避けられない場合のみ限定的に許容する(コメントで理由を書く)
bad
const fetchUser = async (): Promise<any> => {
const res = await fetch("/api/user");
return res.json(); // 返り値の構造が不明
};
good
type User = {
id: string;
name: string;
email: string;
};
const fetchUser = async (): Promise<User> => {
const res = await fetch("/api/user");
const json = (await res.json()) as User; // 明確に User 型にする
return json;
};
型アサーション(as)の乱用禁止
理由
・型安全性が失われる
・TypeScriptの型推論を無視するため保守性が下がる
- まずはデータ構造と型定義を整理して回避できないか見直す
- as constはOK(readonly化のため)
- 回避できない場合はユーザ定義型ガードなどで安全に型を絞る
bad
// unknown な値をそのまま User と決めつけてしまう
const json = await fetch('/api/user').then(res => res.json());
const user = json as User; // 本当は User じゃなくても通る
// 実行時に user.name が undefined なら落ちる危険性
console.log(user.name.toUpperCase());
good
type User = { id: number; name: string };
function isUser(v: unknown): v is User {
return (
typeof v === "object" &&
v !== null &&
"id" in v &&
"name" in v &&
typeof (v as any).id === "number" &&
typeof (v as any).name === "string"
);
}
const data: unknown = await fetch("/api/user").then(r => r.json());
if (isUser(data)) {
console.log(data.name); // 安全
} else {
console.error("Invalid data");
}
型定義は基本的にtypeを使う(type vs interface)
理由
・開発体験の良さ
- 基本的にはtypeでもinterfaceでも問題ない
- プロジェクトで統一すると可読性・保守性が向上する
- VSCodeでホバーしたときの表示の分かりやすさはtypeの方が優れている
- ライブラリ開発の場合は、利用者が拡張しやすいinterfaceを選ぶと良い
null,undefinedは型で表現する(optional ? を使う)
理由
・TypeScriptでは型で値の存在・非存在を表現できる
- Optionalな値には undefined を直接使わず、TypeScriptの ? を活用
bad
// optionalの値にundefinedを直接割り当てている
interface User {
name: string;
age: number | undefined;
}
const user: User = {
name: "Alice",
age: undefined, // 直接undefinedを使用している
};
good
// Optionalな値には?を使う
interface User {
name: string;
age?: number; // ageが無くてもOK
}
const user1: User = {
name: "Alice",
};
const user2: User = {
name: "Bob",
age: 25, // ageがある場合だけ値を持つ
};
関数・ロジック設計
関数は副作用(例:state更新)を減らして純粋性を保つ
理由
・再利用性
・可読性
・テスト容易性
- 1関数1役割を目指す
- APIリクエストを行う関数は「引数を受け取り、レスポンスを返す」だけの純粋関数にする。
- useStateなどの副作用は関数の外側で管理する。
- データ整形は別関数に分ける
bad
// components/MyComponentBad.tsx
const MyComponent = () => {
const [data, setData] = useState<any>(null)
const handleSubmit = async (formValues: any) => {
// データ整形とAPI呼び出しを一緒にやる
const body = {
payload: formValues,
timestamp: new Date().toISOString()
}
try {
// レスポンスからデータを直接取り出す
const response = await axios.request({
url: '/example-endpoint',
method: 'post',
data: body,
})
setData(response.data)
} catch (err) {
console.error('API取得エラー:', err)
}
}
good
// File: apiHelper.ts
// フォームデータをAPI用に変換する関数
export const formatRequestData = (formValues: any) => {
return {
// 必要に応じて変換
payload: formValues,
timestamp: new Date().toISOString(),
}
}
// APIを叩き、レスポンスのみを返す関数
export const getApiData = async (formValues: any) => {
const body = formatRequestData(formValues)
try {
const response = await axios.request({
url: '/example-endpoint',
method: 'post',
data: body,
})
return response.data
} catch (err) {
console.error('API取得エラー:', err)
return null
}
}
// MyComponent.tsx
const MyComponent = () => {
const [data, setData] = useState<any>(null)
const handleSubmit = async (formValues: any) => {
const result = await getApiData(formValues)
setData(result)
}
return (
<div>
<button onClick={() => handleSubmit({ key: 'value' })}>
データ取得
</button>
<pre>{JSON.stringify(data, null, 2)}</pre>
</div>
)
}
早期リターン推奨
理由
・ネストを減らせる
・可読性
・保守性
- 関数の冒頭で「無効値チェック」や「早期終了条件」を先に処理する
- メイン処理はできるだけネスト無しで書く
bad
// 配列の最大値を取得する関数
const getMaxValue = (arr: number[] | null): number => {
// ネストが深くて読みにくい
if (arr) {
if (arr.length > 0) {
let max = arr[0];
for (let i = 1; i < arr.length; i++) {
if (arr[i] > max) {
max = arr[i];
}
}
return max;
} else {
return -Infinity; // 空配列の場合
}
} else {
return -Infinity; // null の場合
}
};
good
// 配列の最大値を取得する関数
const getMaxValue = (arr: number[] | null): number => {
// ガード節で早期 return
if (!arr || arr.length === 0) return -Infinity;
// メイン処理はネストなしでシンプル
let max = arr[0];
for (let i = 1; i < arr.length; i++) {
if (arr[i] > max) {
max = arr[i];
}
}
return max;
};
安易な共通化を避け、変わらない概念だけを抽出する
理由
・似ている処理を無理にまとめると条件分岐だらけの巨大な関数 が生まれ、把握困難になる
・要件変更やデザイン変更時に壊れやすく、保守性が落ちる
・安定して変わらない概念だけを共通化するべき
- 似ていてもまずは別々に実装
- 変わらないことが明確になった“小さな概念のかたまり”だけ 共通化する
- 「共通部分があるから抜き出す」は避ける
- 小さく作るのは良いが、それは責務分離のためであり、再利用のためではない
bad
// ❌ 似ている処理を無理やり共通化した例
// modalType によって条件分岐しすぎており、拡張も変更もつらい。
// すべてのモーダルの仕様変更の影響を受ける "時限爆弾" 化しやすい。
const showModal = (modalType, options) => {
if (modalType === 'alert') {
console.log(`Alert: ${options.message}`);
if (options.confirmButtonText) {
console.log(`Confirm: ${options.confirmButtonText}`);
}
} else if (modalType === 'form') {
console.log(`Form title: ${options.title}`);
if (options.fields) {
console.log(`Fields: ${options.fields.join(', ')}`);
}
if (options.onSubmit) {
options.onSubmit();
}
} else if (modalType === 'image') {
console.log(`Image path: ${options.src}`);
if (options.caption) {
console.log(`Caption: ${options.caption}`);
}
}
// ...今後も種類が増えるたびに条件分岐が増える悪魔の増築...
};
good
//① まずは別々の関数で作る(無理な共通化をしない)
// ✔️ Alertモーダル専門の関数
const showAlertModal = ({ message, confirmText }) => {
console.log(`Alert: ${message}`);
if (confirmText) console.log(`Confirm: ${confirmText}`);
};
// ✔️ Formモーダル専門の関数
const showFormModal = ({ title, fields, onSubmit }) => {
console.log(`Form: ${title}`);
console.log(`Fields: ${fields.join(', ')}`);
onSubmit?.();
};
//② 時間が経つと「共通していた部分」(変わらない概念)が見えてくる
//例えば、
//「モーダルは基本的に open と close ができる」
//という 変わらない概念 が見つかったとする。
//③ そこで初めて“変わらない部分”だけ共通化する
// ✔️ モーダルの「変わらない動作」だけを共通化した小さな部品
const createModalBase = () => ({
open: () => console.log('Modal opened'),
close: () => console.log('Modal closed'),
});
//④ 専用モーダルはこの小さな共通部品を組み合わせる
// ✔️ 特化した責務はそれぞれの関数に持たせる
const createAlertModal = ({ message }) => {
const base = createModalBase();
return {
...base,
show: () => console.log(`Alert: ${message}`),
};
};
const createFormModal = ({ title, fields }) => {
const base = createModalBase();
return {
...base,
show: () => {
console.log(`Form: ${title}`);
console.log(`Fields: ${fields.join(', ')}`);
},
};
};
React Hooks
useEffectは原則使わない
参考:https://ja.react.dev/learn/you-might-not-need-an-effect
理由
・「不必要なレンダリング」「副作用のループ」「依存関係のズレ」が発生しやすくなる
・コードが複雑になる
- まずは使わずに実装できるか考える
- useEffect以外の手段がない場合のみ許容する
bad
useEffect(() => {
setFiltered(items.filter(i => i.active));
}, [items]);
good
const filtered = useMemo(() => items.filter(i => i.active), [items]);
useMemo/useCallbackはむやみに使わない
理由
・Hook自体にもオーバヘッドがある
・軽い処理に使うと逆効果になる場合がある
- 必要な場合に限定して利用する
| フック | 使う | 使わない |
|---|---|---|
| useMemo | ・重い計算をメモ化 ・依存値が変わらない限り同じ結果を使いたい値(計算結果) ・propsで「計算済みデータ」を子に渡す場合 |
・軽い計算(文字列結合、単純なmapなど) ・なんとなく「最適化した気」になって使う ・Reactが再レンダリングしても困らない場合 |
| useCallback | ・子コンポーネントへ「安定した関数」を渡す必要がある場合(メモ化された子など) ・再レンダリングを避けるため関数のidentity安定化が必要な場合 |
・再レンダリングコストが小さいコンポーネントの場合 ・子コンポーネントがmemoされていない場合 単純なinline関数で十分な場合(例:onClick={()=>setOpen(true)}) |
useMemoを使う例
//計算コストが高い処理
const filtered = useMemo(() => heavyFilter(data), [data]);
//props 経由で子コンポーネントに渡すオブジェクトや配列を安定化したい時
const options = useMemo(() => ({ theme }), [theme]);
useCallbackを使う例
//memo 化された子コンポーネントに関数を渡すとき
const handleClick = useCallback(() => doSomething(id), [id]);
<Child onClick={handleClick} />
state化は最小限にする
理由
・余計な再レンダリングが発生
・バグを生みやすい
・コンポーネントが複雑化する
- 不要なstateは持たない
bad
//①:計算で求まる値を state に保存する
const [filtered, setFiltered] = useState(items.filter(...));
//②:レンダリングに不要な値を state に
const [scrollY, setScrollY] = useState(0);
//③:フォーム入力の"表示状態に関係しない"データ
const [isValid, setIsValid] = useState(false);
good
//①
const filtered = useMemo(() => items.filter(...), [items, filter]);
//②
const scrollY = useRef(0);
//③
const isValid = useMemo(() => validate(value), [value]);
- stateの二重管理は避ける
bad
const [count, setCount] = useState(0);
const [isZero, setIsZero] = useState(true);
useEffect(() => setIsZero(count === 0), [count]); // ←二重管理
good
const isZero = count === 0;
- サーバーコンポーネントで保持すべきデータをstate化しない
bad
//サーバーで取得した静的データをクライアントで state 化
const [items, setItems] = useState(props.items);
good
const items = props.items;
カスタムHookの分離ルール
①ビジネスロジックはカスタムhookに分離する
理由
・コンポーネントをUIの描画に集中させるため
・テストを書きやすくするため
・コンポーネントの肥大化を防ぐため
- API呼び出し、副作用、状態管理はカスタムフックへ分離する
- コンポーネントは"UIのみ"を担当する
bad
const Items = () => {
const [items, setItems] = useState([]);
useEffect(() => {
fetch("/api/items")
.then((res) => res.json())
.then(setItems);
}, []);
return <ItemList items={items} />;
};
good
// useItems.ts
export function useItems() {
const [items, setItems] = useState([]);
useEffect(() => {
fetch("/api/items").then(r => r.json()).then(setItems);
}, []);
return { items };
}
// Items.tsx
const Items = () => {
const { items } = useItems();
return <ItemList items={items} />;
};
②カスタムHookは「1責務」だけ持つ
理由
・複合ロジックを1つにまとめるとテストができない
・再利用できない
・修正時に破壊的変更になりやすい
- 1Hook = 1責務になるよう意識する
bad
export function useUser() {
// fetch
// form
// page
// validation
// pagination
}
good
export const useUserFetch = () => {};
export const useUserForm = () => {};
export const useUserPagination = () => {};
③複雑なhookはテスト可能な構造にする
理由
・fetchを内部にべた書きするとテストできない
・副作用をmockできない
- 依存関係を引数で受け取る
- テストで差し替え可能にする
bad
export function useItems() {
useEffect(() => {
fetch("/api/items").then(() => {});
}, []);
}
good
export function useItems(fetcher = fetchItemsApi) {
const [items, setItems] = useState([]);
useEffect(() => {
fetcher().then(setItems);
}, [fetcher]);
return { items };
}
コードスタイル
三項演算子はネストしない
理由
・可読性
- 条件が複雑になる場合は if文 / 変数で事前に値を用意する
- 「nullチェック」や「簡単な条件表示」に使う
bad
const UserStatus: React.FC<{ user?: User }> = ({ user }) => {
// ❌ 三項演算子がネストしていて可読性が低い
return (
<div>
{user
? user.isAdmin
? "管理者ユーザー"
: "一般ユーザー"
: "未ログイン"}
</div>
);
};
good
const UserStatus: React.FC<{ user?: User }> = ({ user }) => {
let status = "未ログイン";
if (user) {
status = user.isAdmin ? "管理者ユーザー" : "一般ユーザー"; // ネストなし
}
return <div>{status}</div>;
};
三項演算子はまず??が使えるか確認する
理由
・可読性
- 冗長になるため、??で書くことを優先する
- 値がないときのデフォルトは??が最も読みやすい
bad
const username = inputName !== null && inputName !== undefined
? inputName
: "名無しさん";
good
const username = inputName ?? "名無しさん";
関数はアロー関数で定義する
理由
・可読性が高い
・型推論が自然
・this を気にせず書ける
・使い慣れてる
- アロー関数vs関数宣言はどちらを選んでも大差ない
- プロジェクトで統一すると可読性・保守性が向上する
引数は分割代入で受け取る
理由
・型の意図が伝わりやすい
・引数の順番に依存しない
・props.の記述が不要になる
- 関数コンポーネントの引数は必ず分割代入で受ける
- 型はtypeで定義し、分割代入時に型注釈を付ける
bad
import React from 'react';
interface ButtonProps {
label: string;
onClick: () => void;
}
// props をそのまま受けて、内部で参照している
const Button = (props: ButtonProps) => {
return (
<button onClick={props.onClick}>
{props.label} {/* props.label 参照が冗長 */}
</button>
);
};
export default Button;
good
import React from 'react';
interface ButtonProps {
label: string;
onClick: () => void;
}
// 引数で分割代入して、props を直接利用
const Button = ({ label, onClick }: ButtonProps) => {
return (
<button onClick={onClick}>
{label} {/* 直接参照できるのでシンプル */}
</button>
);
};
export default Button;
省略記法を使う
理由
・可読性
-
オブジェクト
- キー名と変数名が同じ場合、省略記法を使う
- 省略すると意味が分かりづらい場合はフル記法を許容
badconst name = 'Alice'; const age = 25; const user = { name: name, // 冗長 age: age, // 冗長 }; console.log(user);goodconst name = 'Alice'; const age = 25; // キーと変数名が同じ場合は省略記法を使用 const user = { name, age }; console.log(user); -
boolean型の属性
- trueの場合は省略
bad<button disabled={true}>Click</button>good<button disabled>Click</button> -
配列型の定義
- 基本は[]形式
badconst numbers: Array<number> = [1, 2, 3];goodconst numbers: number[] = [1, 2, 3];
文字列操作はテンプレート文字列を使用する
理由
・可読性
- 文字列の連結に + は使わない
-
${}を使う
bad
// ユーザー情報を表示するサンプル
const firstName: string = "太郎";
const lastName: string = "山田";
const age: number = 25;
// テンプレートリテラルを使った文字列連結
const message = 'こんにちは、'+lastName+firstName+'さん。あなたは'+age+'歳ですね。';
const name = [firstName,lastName].join();
good
// ユーザー情報を表示するサンプル
const firstName: string = "太郎";
const lastName: string = "山田";
const age: number = 25;
// テンプレートリテラルを使った文字列連結
const message = `こんにちは、${lastName} ${firstName}さん。あなたは${age}歳ですね。`;
cosnt name = `${lastName} ${firstName}`
命名規則
ファイル
- コンポーネント
PascalCase- 例:Button.tsx,ModalContent.tsx
- Hook
-
camelCase+use プレフィックス - 例:useAuth.ts
-
- utils
camelCase- 例:formatDate.ts
変数、プロパティ
- 参考:
- 変数:
camelCase- 例:userData
- 定数:
UPPER_SNAKE_CASE- 例:API_BASE_URL
- 真偽値:
is、has、should プレフィックス- 例:isLoading
- イベントハンドラ:
on、handle プレフィックス- 例:handleSubmit、onChange
- 配列
- 複数形
- 例:users
- Next.jsのapp/ 配下に格納されるフォルダ名(urlになる部分)
- kebab-case
- shopping-cart
テンプレート
-
理由
・---
bad
bad
good
good