リファクタリングスキル は何をしますか?
このスキルは、確立されたパターンに機能を合わせ、重複を排除し、コードの再利用性を向上させるためのリファクタリングを行う際に使用します。
UIがpattern-libraryとローカル構成のどちらに属するかについては、design-systemスキルを使用してください。
リファクタリング前の分析
1. 類似機能の特定
- コードベース内で同様の機能を持つ比較可能な機能を見つける
- ディレクトリ構造、コンポーネントパターン、ユーティリティの使用法を比較する
- 同じような機能を持つが実装が異なる機能を探す
- セマンティック検索を使用して関連機能を検索する:「features with similar table/data grid patterns」のようなパターン
2. 重複機会の発見
重複コードパターンを検索する:
- コンポーネント: 機能間で類似したコンポーネントファイルを探す(同じ名前または類似機能)
- フック: 重複したフックロジックを確認する(テーブルフック、フィルターフック、集計フック)
- ユーティリティ: 重複したユーティリティ関数を見つける(集計、フォーマッター、バリデーター、トランスフォーマー)
- スタイル: 重複した
sxプロップパターンまたはインラインスタイルを特定する - 定数: 重複した定数または設定オブジェクトを確認する
- 型: 重複した型定義またはインターフェースを探す
3. 共有ユーティリティの確認
新しいユーティリティを作成する前に、既存のものを確認する:
- ドメイン固有のユーティリティについて
@/util/ディレクトリを検索する - 日付書式関数について
@/util/datesを確認する - 数値書式関数について
@/util/numbersを確認する - 再利用可能なコンポーネントについて
@/components/を確認する - デザインシステムコンポーネントについて
pattern-libraryを確認する - 新しく作成する前に、grep/検索を使用して既存の実装を見つける
リファクタリングプロセス
ステップ 1:共有コードの抽出
重複を見つけた場合:
-
適切な
@/util/ディレクトリに共有ユーティリティを作成する:// 例:util/[domain]/[category].ts export const sharedFunction = () => { ... }; export const sharedConstant = { ... }; -
すべての機能を更新して共有ロケーションからインポートする:
// 機能固有のファイル - 共有を再エクスポート export { sharedFunction, sharedConstant } from "@/util/[domain]/[category]"; -
機能ディレクトリから重複実装を削除する
-
コードベース全体のすべてのインポートを更新する
ステップ 2:コンポーネントの統合
重複コンポーネントを見つけた場合:
- 最も完全で機能豊富なバージョンを特定する(プロップ、機能、テストを確認)
- 共有ロケーションに抽出する(
@/components/または機能に依存しないロケーション) - プロップを柔軟にする(ユニオン型、オプショナルプロップ、またはジェネリクスを使用)
- そのコンポーネントを使用するすべての機能でインポートを更新する
- 重複コンポーネントファイルを削除する
- すべての使用箇所が正しく動作することを確認する
ステップ 3:ローディングパターンの調整
ローディング状態について:
loading.tsxファイルを確認する - シンプルなスケルトンであるべきで、ページ全体のレイアウトではない- コンポーネントレベルのスケルトンローダーを削除する - 初期ロードにはページレベルの
loading.tsxに依存する - 再フェッチ中: 既存のデータを表示し、スケルトンは表示しない(UX上明示的に必要な場合を除く)
- コードベース内の類似機能のパターンに合わせる
- Next.js App Routerの規約に従う -
loading.tsxが初期ロードを処理し、コンポーネントが再フェッチ状態を処理する
ステップ 4:スタイルの統合
スタイルについて:
- 再利用可能なスタイルパターンを共有コンポーネントまたはスタイルユーティリティに抽出する
- 共有コンポーネントが存在する場合はインライン
sxプロップを削除する - カスタムスタイルの代わりに
pattern-libraryのデザインシステムコンポーネントを使用する - 共有ユーティリティまたはテーマ拡張にできる重複したスタイルオブジェクトを確認する
- Material-UIパターンに従う - テーマ値を使用し、マジックナンバーを避ける
ステップ 5:パターンの整合性検証
確立された機能との一貫性を確保する:
- ディレクトリ構造を比較する - 同様のパターンに従う必要がある(
_components/、_hooks/、_utils/) - フックパターンを確認する - 機能要件が異なる場合は、よりシンプルなフックでも問題ない
- データフローを検証する - 可能な限り類似機能と一致させる必要がある
- コンポーネント構成を見直す - 同様のパターンを使用する必要がある(ラッパー、構成など)
- 命名規則を確認する - 確立されたパターンに従う(コンポーネントはPascalCase、フックはcamelCase)
ステップ 6:ユーティリティ関数の統合
ユーティリティ関数について:
- 共有ユーティリティに関数が存在するか確認する(コードベースを検索)
- 実装を比較する - より完全で堅牢なバージョンを使用する
- 重複を統合する - 共有ロケーションに移動し、すべてのインポートを更新する
- 差異を文書化する - 実装が意図的に異なる場合は、その理由を文書化する
- 機能を維持する - 統合されたバージョンがすべてのユースケースを処理できることを確認する
ステップ 7:コメントのクリーンアップ
リファクタリング後、不要なコメントを削除する:
- マイグレーションノートを削除する - コードがどこから移動されたかを説明するコメント(例:「isCollectingはTable/utils.tsからエクスポートされるようになりました」)
- 自明なコメントを削除する - コードが何をしているかを単に言い換えるだけのコメント(例:コードが明らかに再エクスポートを示しているのに「// 共有集計を再エクスポート」)
- 説明コメントは残す - なぜ何かを行っているのかを説明するコメント(例:「// setTimeoutを使用して、フォーム状態が更新されることを確認する」)
- 複雑なロジックのコメントは残す - 自明でないビジネスロジックやエッジケースを説明するコメント
- TODO/FIXMEコメントを削除する - 正当な将来の作業項目でない限り
不要なコメントの例:
// ❌ 不要 - コードから明らか
// utilから共有集計を再エクスポート
export { latest, totalRange } from "@/util/keywords/gridAggregations";
// ❌ 不要 - マイグレーションノート、もう必要ない
// isCollectingは、重複を避けるためにTable/utils.tsからエクスポートされるようになりました
// そこからインポート:import { isCollecting } from "../KeywordListDetail/Table/utils";
良いコメントの例:
// ✅ 良い - WHYを説明している
// CSV処理後にフォーム状態が確実に更新されるようにsetTimeoutを使用
setTimeout(() => {
void handleSubmit(onSubmit)();
}, 100);
// ✅ 良い - 複雑なロジックを説明している
// キーワードは以下の場合に更新されます:
// 1. lastUpdated値が変更された場合(最も信頼できる指標)、または
// 2. 収集状態ではなくなり、十分な回数ポーリングした場合(フォールバック)
ステップ 8:コード品質の向上
リファクタリングされたコード全体に一貫したコード品質パターンを適用する:
Null/Undefinedチェック
緩やかな等価性を使用してnullとundefinedのチェックを簡略化する:
// ❌ 冗長
if (value !== undefined && value !== null) { ... }
if (value !== undefined && value !== null && value !== "") { ... }
// ✅ 簡略化
if (value != null) { ... }
if (value != null && value !== "") { ... }
注記: 1つのチェックでnullとundefinedの両方を確認するには!= null(緩やかな等価性)を使用します。これは慣用的なTypeScript/JavaScriptであり、より簡潔です。
型のみのインポート
ビルドパフォーマンスを向上させ、意図を明確にするために、型のみのインポートにはimport typeを使用する:
// ❌ 型のみの使用でランタイムインポート
import { SomeType } from "app-api/types/domain";
// ✅ 型のみのインポート
import type { SomeType } from "app-api/types/domain";
適用先:
app-apiからの型定義- インターフェースのインポート
- 型のみのユーティリティ型
- 型アノテーションでのみ使用されるインポート
不要な型アサーションの削除
TypeScriptが正しく型を推論できる場合は、型アサーションを削除する:
// ❌ 不要なアサーション
const items = data.map((item) => item as SomeType);
// ✅ TypeScriptに推論させる
const items = data.map((item) => item);
アサーションを保持する場合: TypeScriptが真に型を推論できない場合、またはTypeScriptが検証できない方法で型を絞り込む場合のみ。
不要なNullish Coalescingの削除
値がすでにnull許容である場合、またはTypeScriptが正しい型を推論する場合は、?? nullを削除する:
// ❌ 不要 - valueGetterはすでにnull許容を返す
valueGetter: (value, row) => {
return row.someField ?? null;
};
// ✅ 簡略化
valueGetter: (value, row) => {
return row.someField;
};
保持する場合: API互換性のためにundefinedを明示的にnullに変換する場合、または型システムがそれを要求する場合。
未使用のプロップの削除
使用されていないプロップをコンポーネントインターフェースと実装から削除する:
// ❌ 未使用のプロップ
interface ComponentProps {
item: Item;
unusedField: string; // コンポーネントで使用されていない
}
// ✅ クリーンなインターフェース
interface ComponentProps {
item: Item;
}
コールバックに明示的な型を追加
TypeScriptが推論できない場合、コールバックパラメータに明示的な型アノテーションを追加する:
// ❌ 暗黙のany
const items = ids.map((id) => { ... });
// ✅ 明示的な型
const items = ids.map((id: string | number) => { ... });
適用先: 配列メソッド(map、filter、forEach)、イベントハンドラー、および型がコンテキストから明確でないその他のコールバック。
繰り返されるJSXパターンの抽出
繰り返されるJSXパターンを再利用可能なコンポーネントに抽出する:
// ❌ 繰り返されるパターン
{
isAggregation ? (
<Typography
variant="body2"
fontWeight="medium"
fontStyle="italic"
color="text.secondary"
>
{formattedValue}
</Typography>
) : (
<Typography variant="body2" fontWeight="medium">
{formattedValue}
</Typography>
);
}
// ✅ 抽出されたコンポーネント
<AggregationTypography
formattedValue={formattedValue}
isAggregation={isAggregation}
/>;
バリエーションのある実装にはファクトリ関数を使用
ロジックは似ているが異なるデータ構造を処理する必要がある場合は、ファクトリ関数を使用する:
// ✅ ファクトリ関数パターン
export const createIntentOperators = <T extends { intent?: Intent | Intent[] }>(
normalizeToArray: (value: T) => Intent[]
) => {
return {
// 正規化された配列で動作する演算子
};
};
// 異なる機能での使用法
const operators1 = createIntentOperators<Feature1Type>((item) => [item.intent]);
const operators2 = createIntentOperators<Feature2Type>(
(item) => item.intents || []
);
利点: 異なるデータ構造を適切に処理しながら、コアロジックを再利用します。
コードレビューハードニング(繰り返しのフィードバック)
リファクタリングがグリッド、ダッシュボード、フェッチ、または共有の「行+メトリクス」形状に触れる場合は、これらを適用します(繰り返されるMoz Pro UI PRフィードバックに準拠)。
DataGrid:詳細パネルトグル
- 詳細パネル列の展開/折りたたみには、
pattern-libraryのDetailPanelToggleを優先して使用し、カスタムセルでアドホックなIconButton+@mui/iconsを使用しない。 - アクセシビリティを設定する: セルがタブフォーカス可能であるべき場合は、
buttonTabIndex={0}を渡す(app/(default)/competitive-research/top-competing-content/_module/components/Table/columns.tsx、SerpFeaturesCellを参照)。 - レイアウト/センタリング:
Box/Stack(または、それが確立されたパターンである場合はStackのdirection/spacing)で**sxを使用する。BoxのレガシーなMUIシステムスタイルプロップ**(例:display、alignItemsをトップレベルプロップとして)をsxに置き換えないでください。MUIはそのアプローチを廃止し、**sx**を推奨しています。
型付け:object / 匿名の行形状を避ける
- 構造的な型が存在するか、一度定義できる場合に、
brand_definitions、行フラグメント、またはAPIに近いリストに対してreadonly object[]をスレッド化しない。 - 機能の
utilsモジュールで共有の「like」インターフェース(例:BrandDefinitionLike)を定義し、エクスポートし、セル内でgetBrandNameをキャスト付きで複製する代わりに、単一の表示行ヘルパー(例:getBrandDefinitionDisplayLine)を用意する。 - パラメータに型付けされた後は、冗長なキャスト(
as BrandDefinitionLike[])を削除する。 - 形状が実際の
BrandDefinitionである場合は、*_module/utils/dataHelpersからドメインヘルパーを再利用する。プロンプト行のペイロードが異なる場合にのみ、より緩やかなヘルパーを使用する。
Typography vs Box (pattern-library)
- テキスト(空状態、インラインの「—」、強調されたスパン)には、適切な場合に
pattern-libraryからcomponent="span"を指定した**Typography**を優先し、コピーを装ったBoxは使用しない。 TypographyAPI: 意味を表現する場所では**variant、color、fontWeight、component**を使用します(これらは非推奨ではありません)。Box/ レイアウトラッパー: スペーシング、フレックスレイアウト、サイズ設定、テーマ値には**sx**を優先する。スタイリングをsxからBoxの「スタイル」ショートハンドプロップに移動するような、ついでのリファクタリングは避けてください。それは現在のMUIガイダンスに矛盾します。
コンポーネント境界:プロップと内部状態
- データからブール値を導出する: 親がレイアウトやURL状態にそれらを必要としない場合(例:プライマリブランドの
brand_termsからの「ブランド用語がある」)、コンポーネント内で導出する。 - UIのみのトグルは内部に保つ(例:1つのカード内で2つのチャートモードを切り替えるなど)。別の兄弟またはルートが同じ状態を駆動する必要がある場合を除き、その場合はリフティングが必要な理由を文書化する。
制御フローとJSX
- ローディング/空のブランチには、単一の
returnの直前に大きな三項演算子を割り当てる代わりに、**早期return**を優先する。ただし、メモ化が本当に1つの式を必要とする場合はその限りではない。 - ソートコンパレーター: 想定されるドメインの最大値に結び付けられたマジックセンチネル(例:
5)をハードコードしない。null/undefinedには**Number.POSITIVE_INFINITY/ 明示的な「最後にソート」**を使用し、制限が変更されても動作が正しいままになるようにする。
フェッチとページネーション
- バックエンドがページングされており、プロジェクトが1ページを超える可能性がある場合、ダッシュボード規模のリストに対して固定の小さな
limit(例:100)を避ける。 - ページが制限より少ない行またはゼロ行を返すまで、安定したページサイズでループするか、アクションの文書化されたコントラクトに従う(最大
limitについてはmoz-pro-serviceアクションドキュメント/Brunoを参照)。 - 呼び出し元がカウントを必要とする場合は、一貫して**
total**または派生した合計を表面化する。
プレゼンテーションラッパー:明白なことを繰り返さない
- 親の
Cardがすでに表示タイトルを設定している場合、冗長な代替状態フィールドを削除する(例:カードタイトルと一致するcardContentTitleの重複を避ける)。 - Nullチェック: プロップが
stringと型付けされている場合、ランタイムで非文字列を受け入れる可能性のあるif (prop) ...やif (prop !== undefined) ...のチェックは行わない。