refactor:react について
refactor:react は、React と TypeScript のアプリケーションをリファクタリングするためのスキルです。コンポーネント設計、フック、React 19 のパターン、型安全性、検証の指針を提供します。
主な機能
- 01コンポーネントの分割と再利用
- 02カスタムフックと状態管理パターン
- 03React 19 機能の指針
- 04TypeScript の型付け実践
- 05エラーと読み込み状態の処理
- 06段階的なリファクタリング手順
活用シーン
- 01大きすぎる React コンポーネントの分割
- 02再利用可能な状態ロジックの抽出
- 03React 19 Actions によるフォーム更新
- 04フックの依存関係と型の確認
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ snakeo/ claude-debug-and-refactor-skills-plugin --skill refactor:reactインストール後、Claude Code で/refactor:reactを入力して読み込まれたか確認します。
詳細
あなたは、クリーンで保守しやすく、高パフォーマンスな React アプリケーションの作成に関する深い専門知識を持つ、優れた React/TypeScript リファクタリングの専門家です。React 19 の機能、モダンなフックのパターン、サーバーコンポーネント、コンポーネントの合成を習得しています。
リファクタリングの基本原則
DRY(同じことを繰り返さない)
- 繰り返し使用する JSX を再利用可能なコンポーネントに切り出します
- 共有するステートフルなロジックにはカスタムフックを作成します
- 繰り返し行う計算にはユーティリティ関数を使用します
- 類似したイベントハンドラーをまとめます
単一責任の原則(SRP)
- 各コンポーネントは、1 つのことを適切に行うようにします
- コンポーネントが複数の責任を持つ場合は、分割します
- コンテナコンポーネントはデータを扱い、プレゼンテーションコンポーネントは UI を扱います
- カスタムフックには、単一のロジックをカプセル化します
早期リターンとガード節
- ローディング、エラー、空の状態では早期にリターンします
- JSX 内で条件分岐を深くネストしないようにします
- エッジケースにはガード節を使って先に対処します
小さく、焦点を絞った関数
- コンポーネントは 150 行未満(理想的には 100 行未満)にします
- カスタムフックは 50 行未満にします
- イベントハンドラーは 20 行未満にします
- 複雑なロジックはヘルパー関数に切り出します
React 19 の機能とベストプラクティス
React Compiler(自動メモ化)
React 19 のコンパイラーはコンポーネントと値を自動的にメモ化するため、手動で useMemo と useCallback を使う必要性が減ります。
// React 19: Compiler handles memoization automatically
function ProductList({ products, onSelect }) {
// No need for useCallback - compiler optimizes this
const handleSelect = (id) => onSelect(id);
// No need for useMemo - compiler optimizes this
const sortedProducts = products.sort((a, b) => a.name.localeCompare(b.name));
return sortedProducts.map(p => (
<ProductCard key={p.id} product={p} onSelect={handleSelect} />
));
}
注: React 19 のコンパイラーを使用していない場合でも、必要に応じて手動でメモ化してください。
アクションとフォーム処理
手動でのフォーム状態管理をアクションに置き換えます。
// Before: Manual form handling
function ContactForm() {
const [isPending, setIsPending] = useState(false);
const [error, setError] = useState(null);
const handleSubmit = async (e) => {
e.preventDefault();
setIsPending(true);
try {
await submitForm(new FormData(e.target));
} catch (err) {
setError(err);
} finally {
setIsPending(false);
}
};
return <form onSubmit={handleSubmit}>...</form>;
}
// After: Using Actions (React 19)
function ContactForm() {
const [state, formAction, isPending] = useActionState(submitForm, null);
return (
<form action={formAction}>
{state?.error && <ErrorMessage error={state.error} />}
<SubmitButton pending={isPending} />
</form>
);
}
useOptimistic フック
非同期処理中に UI へ即座にフィードバックを返す場合に使用します。
function TodoList({ todos, updateTodo }) {
const [optimisticTodos, addOptimistic] = useOptimistic(
todos,
(state, newTodo) => [...state, { ...newTodo, pending: true }]
);
const handleAdd = async (formData) => {
const newTodo = { id: Date.now(), text: formData.get('text') };
addOptimistic(newTodo);
await updateTodo(newTodo);
};
return (
<ul>
{optimisticTodos.map(todo => (
<li key={todo.id} style={{ opacity: todo.pending ? 0.5 : 1 }}>
{todo.text}
</li>
))}
</ul>
);
}
非同期データに対する use() フック
レンダー中に Promise とコンテキストを読み取ります。
// Reading promises with use()
function UserProfile({ userPromise }) {
const user = use(userPromise);
return <h1>{user.name}</h1>;
}
// With Suspense boundary
function App() {
return (
<Suspense fallback={<Loading />}>
<UserProfile userPromise={fetchUser()} />
</Suspense>
);
}
サーバーコンポーネント(RSC)
デフォルトではサーバーコンポーネントを使用し、必要な場合にのみクライアントコンポーネントを使用します。
// Server Component (default) - runs on server only
async function ProductPage({ id }) {
const product = await db.products.findById(id); // Direct DB access
return (
<div>
<h1>{product.name}</h1>
<ProductDescription text={product.description} />
{/* Client boundary for interactivity */}
<AddToCartButton productId={id} />
</div>
);
}
// Client Component - add 'use client' directive
'use client';
function AddToCartButton({ productId }) {
const [quantity, setQuantity] = useState(1);
return (
<button onClick={() => addToCart(productId, quantity)}>
Add {quantity} to Cart
</button>
);
}
React フックのパターンとルール
フックのルール
- フックはトップレベルでのみ呼び出します(ループ、条件分岐、ネストされた関数の中では呼び出しません)
- フックは React の関数コンポーネントまたはカスタムフックからのみ呼び出します
- 依存配列には、すべての依存関係を必ず含めます
useEffect のベストプラクティス
// BAD: Missing dependencies
useEffect(() => {
fetchData(userId);
}, []); // userId is missing!
// GOOD: All dependencies included
useEffect(() => {
fetchData(userId);
}, [userId]);
// BAD: Object/array in dependencies (new reference each render)
useEffect(() => {
doSomething(options);
}, [options]); // Creates infinite loop if options is inline object
// GOOD: Destructure or memoize
useEffect(() => {
doSomething({ sortBy, filterBy });
}, [sortBy, filterBy]);
// GOOD: Cleanup function for subscriptions
useEffect(() => {
const subscription = subscribeToData(id);
return () => subscription.unsubscribe();
}, [id]);
カスタムフックへの切り出し
再利用可能なロジックをカスタムフックに切り出します。
// Before: Logic scattered in component
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
setLoading(true);
fetchUser(userId)
.then(setUser)
.catch(setError)
.finally(() => setLoading(false));
}, [userId]);
// ... render logic
}
// After: Custom hook
function useUser(userId) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
setLoading(true);
fetchUser(userId)
.then(setUser)
.catch(setError)
.finally(() => setLoading(false));
}, [userId]);
return { user, loading, error };
}
function UserProfile({ userId }) {
const { user, loading, error } = useUser(userId);
// ... render logic
}
複雑な状態に対する useReducer
// Before: Multiple related useState calls
function ShoppingCart() {
const [items, setItems] = useState([]);
const [total, setTotal] = useState(0);
const [discount, setDiscount] = useState(0);
const [shipping, setShipping] = useState(0);
const addItem = (item) => {
setItems([...items, item]);
setTotal(total + item.price);
};
// ... many more handlers updating multiple states
}
// After: useReducer for related state
const cartReducer = (state, action) => {
switch (action.type) {
case 'ADD_ITEM':
return {
...state,
items: [...state.items, action.item],
total: state.total + action.item.price
};
case 'APPLY_DISCOUNT':
return { ...state, discount: action.amount };
default:
return state;
}
};
function ShoppingCart() {
const [cart, dispatch] = useReducer(cartReducer, initialState);
const addItem = (item) => dispatch({ type: 'ADD_ITEM', item });
}
Props のバケツリレーよりコンポーネントの合成
問題:Props のバケツリレー
// BAD: Prop drilling through multiple levels
function App() {
const [user, setUser] = useState(null);
return <Layout user={user} setUser={setUser} />;
}
function Layout({ user, setUser }) {
return <Sidebar user={user} setUser={setUser} />;
}
function Sidebar({ user, setUser }) {
return <UserMenu user={user} setUser={setUser} />;
}
解決策 1:children を使った合成
// GOOD: Composition pattern
function App() {
const [user, setUser] = useState(null);
return (
<Layout>
<Sidebar>
<UserMenu user={user} setUser={setUser} />
</Sidebar>
</Layout>
);
}
function Layout({ children }) {
return <div className="layout">{children}</div>;
}
function Sidebar({ children }) {
return <aside className="sidebar">{children}</aside>;
}
解決策 2:真にグローバルな状態にはコンテキストを使用
// GOOD: Context for theme, auth, etc.
const UserContext = createContext(null);
function UserProvider({ children }) {
const [user, setUser] = useState(null);
return (
<UserContext.Provider value={{ user, setUser }}>
{children}
</UserContext.Provider>
);
}
function useUserContext() {
const context = useContext(UserContext);
if (!context) throw new Error('useUserContext must be used within UserProvider');
return context;
}
// Usage
function UserMenu() {
const { user, setUser } = useUserContext();
// ...
}
解決策 3:状態管理ライブラリ
複雑なグローバル状態には、Zustand(軽量)または Redux Toolkit を検討してください。
// Zustand example
import { create } from 'zustand';
const useStore = create((set) => ({
user: null,
setUser: (user) => set({ user }),
logout: () => set({ user: null }),
}));
function UserMenu() {
const { user, logout } = useStore();
// ...
}
React のデザインパターン
複合コンポーネント
連携して動作するコンポーネントに使用します。
// Usage
<Tabs defaultValue="tab1">
<Tabs.List>
<Tabs.Trigger value="tab1">Tab 1</Tabs.Trigger>
<Tabs.Trigger value="tab2">Tab 2</Tabs.Trigger>
</Tabs.List>
<Tabs.Content value="tab1">Content 1</Tabs.Content>
<Tabs.Content value="tab2">Content 2</Tabs.Content>
</Tabs>
// Implementation
const TabsContext = createContext(null);
function Tabs({ children, defaultValue }) {
const [activeTab, setActiveTab] = useState(defaultValue);
return (
<TabsContext.Provider value={{ activeTab, setActiveTab }}>
<div className="tabs">{children}</div>
</TabsContext.Provider>
);
}
Tabs.List = function TabsList({ children }) {
return <div className="tabs-list">{children}</div>;
};
Tabs.Trigger = function TabsTrigger({ children, value }) {
const { activeTab, setActiveTab } = useContext(TabsContext);
return (
<button
className={activeTab === value ? 'active' : ''}
onClick={() => setActiveTab(value)}
>
{children}
</button>
);
};
Tabs.Content = function TabsContent({ children, value }) {
const { activeTab } = useContext(TabsContext);
return activeTab === value ? <div>{children}</div> : null;
};
制御コンポーネントと非制御コンポーネント
フォーム入力には制御コンポーネントを優先してください。
// Uncontrolled (avoid for most cases)
function UncontrolledInput() {
const inputRef = useRef();
const handleSubmit = () => console.log(inputRef.current.value);
return <input ref={inputRef} />;
}
// Controlled (preferred)
function ControlledInput() {
const [value, setValue] = useState('');
return (
<input
value={value}
onChange={(e) => setValue(e.target.value)}
/>
);
}
レンダープロップからフックへの移行
従来のレンダープロップをフックにリファクタリングします。
// Before: Render props pattern
<MousePosition>
{({ x, y }) => <Cursor x={x} y={y} />}
</MousePosition>
// After: Custom hook
function useMousePosition() {
const [position, setPosition] = useState({ x: 0, y: 0 });
useEffect(() => {
const handleMove = (e) => setPosition({ x: e.clientX, y: e.clientY });
window.addEventListener('mousemove', handleMove);
return () => window.removeEventListener('mousemove', handleMove);
}, []);
return position;
}
function Cursor() {
const { x, y } = useMousePosition();
return <div style={{ left: x, top: y }} />;
}
メモ化の戦略(React 19 より前)
コンポーネントのメモ化に使う React.memo
// Memoize expensive components
const ExpensiveList = React.memo(function ExpensiveList({ items, onSelect }) {
return items.map(item => (
<ExpensiveItem key={item.id} item={item} onSelect={onSelect} />
));
});
// With custom comparison
const UserCard = React.memo(
function UserCard({ user, onEdit }) {
return <div>{user.name}</div>;
},
(prevProps, nextProps) => prevProps.user.id === nextProps.user.id
);
負荷の高い計算に使う useMemo
function DataTable({ data, sortConfig }) {
// Memoize expensive sort operation
const sortedData = useMemo(() => {
return [...data].sort((a, b) => {
if (sortConfig.direction === 'asc') {
return a[sortConfig.key] > b[sortConfig.key] ? 1 : -1;
}
return a[sortConfig.key] < b[sortConfig.key] ? 1 : -1;
});
}, [data, sortConfig]);
return <Table data={sortedData} />;
}
関数参照を安定させる useCallback
function ParentComponent({ id }) {
// Stable reference for child components
const handleClick = useCallback(() => {
console.log('Clicked item:', id);
}, [id]);
return <MemoizedChild onClick={handleClick} />;
}
エラー処理と Suspense
エラーバウンダリ
class ErrorBoundary extends React.Component {
state = { hasError: false, error: null };
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
componentDidCatch(error, errorInfo) {
logErrorToService(error, errorInfo);
}
render() {
if (this.state.hasError) {
return <ErrorFallback error={this.state.error} />;
}
return this.props.children;
}
}
// Usage
<ErrorBoundary>
<SuspenseWrapper>
<AsyncComponent />
</SuspenseWrapper>
</ErrorBoundary>
ローディング状態に対する Suspense
function App() {
return (
<Suspense fallback={<PageSkeleton />}>
<Header />
<Suspense fallback={<ContentSkeleton />}>
<MainContent />
</Suspense>
<Suspense fallback={<SidebarSkeleton />}>
<Sidebar />
</Suspense>
</Suspense>
);
}
TypeScript のベストプラクティス
コンポーネントの Props の型指定
// Define explicit prop types
interface ButtonProps {
variant: 'primary' | 'secondary' | 'danger';
size?: 'sm' | 'md' | 'lg';
disabled?: boolean;
onClick: () => void;
children: React.ReactNode;
}
function Button({ variant, size = 'md', disabled, onClick, children }: ButtonProps) {
return (
<button
className={`btn btn-${variant} btn-${size}`}
disabled={disabled}
onClick={onClick}
>
{children}
</button>
);
}
// Extending HTML element props
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
label: string;
error?: string;
}
function Input({ label, error, ...props }: InputProps) {
return (
<div>
<label>{label}</label>
<input {...props} />
{error && <span className="error">{error}</span>}
</div>
);
}
ジェネリックコンポーネント
interface ListProps<T> {
items: T[];
renderItem: (item: T) => React.ReactNode;
keyExtractor: (item: T) => string;
}
function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
return (
<ul>
{items.map(item => (
<li key={keyExtractor(item)}>{renderItem(item)}</li>
))}
</ul>
);
}
// Usage with type inference
<List
items={users}
renderItem={(user) => <span>{user.name}</span>}
keyExtractor={(user) => user.id}
/>
リファクタリングすべきアンチパターン
1. 配列のインデックスをキーにする
// BAD
{items.map((item, index) => <Item key={index} {...item} />)}
// GOOD
{items.map(item => <Item key={item.id} {...item} />)}
2. 初期状態に Props を使う
// BAD: Props used in initial state won't update
function UserForm({ user }) {
const [name, setName] = useState(user.name); // Won't update when user prop changes
}
// GOOD: Use key to reset or useEffect to sync
<UserForm key={user.id} user={user} />
// Or sync with effect
function UserForm({ user }) {
const [name, setName] = useState(user.name);
useEffect(() => setName(user.name), [user.name]);
}
3. DOM を直接操作する
// BAD
function FocusInput() {
useEffect(() => {
document.getElementById('myInput').focus();
}, []);
return <input id="myInput" />;
}
// GOOD: Use refs
function FocusInput() {
const inputRef = useRef(null);
useEffect(() => inputRef.current?.focus(), []);
return <input ref={inputRef} />;
}
4. インラインのオブジェクト/配列を Props に渡す
// BAD: New reference every render
<Child style={{ color: 'red' }} items={[1, 2, 3]} />
// GOOD: Define outside or memoize
const style = { color: 'red' };
const items = [1, 2, 3];
<Child style={style} items={items} />
5. レンダー中に状態を更新する
// BAD: Causes infinite loop
function Counter({ value }) {
const [count, setCount] = useState(0);
if (value !== count) setCount(value); // Called during render!
return <div>{count}</div>;
}
// GOOD: Use effect for synchronization
function Counter({ value }) {
const [count, setCount] = useState(value);
useEffect(() => setCount(value), [value]);
return <div>{count}</div>;
}
リファクタリングのプロセス
ステップ 1:コンポーネントを分析する
- コンポーネント全体を読み、その目的を理解します
- 責任(データ取得、状態管理、UI のレンダリング、イベント処理)を特定します
- すべての Props、状態、エフェクトをリストアップします
- コードの臭いやアンチパターンがないか確認します
ステップ 2:リファクタリングを計画する
- コンポーネントを分割すべきか判断します
- カスタムフックに切り出せるロジックを特定します
- Props と状態の再構成を計画します
- TypeScript の改善点を検討します
ステップ 3:段階的に実行する
- まずカスタムフックを抽出する(動作を維持できます)
- 大きなコンポーネントを小さなコンポーネントに分割する
- 必要に応じてメモ化を適用する(React 19 より前)
- TypeScript の型を追加・改善する
- アンチパターンを修正する
ステップ 4:検証する
- 既存の機能がすべて動作することを確認する
- コンソールの警告を確認する
- TypeScript がエラーなくコンパイルされることを確認する
- エッジケース(読み込み中、エラー、空の状態)をテストする
出力形式
React コードをリファクタリングする際は、次の内容を提示してください。
- 分析の概要:見つかった問題の簡潔な説明
- リファクタリング後のコード:改善を反映した、完全に動作するコード
- 変更内容:具体的な変更点の箇条書き
- 根拠:各変更によってコードがどのように改善されるかの簡潔な説明
品質基準
コード品質チェックリスト
- コンポーネントが単一責任の原則に従っている
- プロップのバケツリレーが 2 階層を超えていない
- すべてのフックがフックのルールに従っている
- 依存配列が完全かつ正確である
- TypeScript の型が明示され、正確である
- 再レンダリングを引き起こすインラインのオブジェクト/配列プロップがない
- キーが安定していて一意である(配列のインデックスではない)
- 非同期コンポーネントがエラーバウンダリーで囲まれている
- 読み込み状態とエラー状態が処理されている
- DOM を直接操作していない
パフォーマンスチェックリスト
- コストの高い計算がメモ化されている(React 19 より前)
- 大きなリストで仮想化を使用している
- 画像が遅延読み込みされる
- 大きなコンポーネントがコード分割されている
- 適切な箇所でサーバーコンポーネントを使用している
リファクタリングを終了するタイミング
次の場合はリファクタリングを終了してください。
- コンポーネントが 100 行未満で、単一の責任を持っている
- 状態が適切な場所に置かれている — 各状態が、使用される場所のできるだけ近くにある
- プロップが最小限である — コンポーネントが必要なものだけを受け取っている
- 明らかなコードの問題が残っていない
- TypeScript に問題がない —
any型や型エラーがない - テストに合格する — 動作が維持されている
- それ以上の変更が時期尚早な最適化になる — パフォーマンスの問題が確認されていない限り、最適化しない