RORK LABEN
TOOLING — Rorkの開発者向けリポジトリが動き続けています。rork-xcodeが7月16日、rork-deviceが7月15日、rork-plistが7月13日に更新されましたOPUS46 — RorkでClaude Opus 4.6が稼働しています。Rork MaxはClaude Codeを土台にアプリを組み立てる設計ですSIM — ブラウザ上で動くクラウドのiOSシミュレータを備え、実機へのインストールは1クリック、App Storeへの提出は2クリックと案内されていますMAX — Rork MaxはReact Nativeではなく純粋なSwiftを出力します。iPhone・iPad・Apple Watch・Apple TV・Vision Pro、そしてiMessageまでが射程ですNATIVE — HealthKit、ARKitとLiDAR、NFC、Dynamic Island、Live Activities、Metalによる3D、Core MLのオンデバイス推論まで扱えますSEED — RorkはLeft Lane Capitalが主導する1,500万ドルのシードラウンドを実施し、Peak XVとa16z Speedrunが参加しましたTOOLING — Rorkの開発者向けリポジトリが動き続けています。rork-xcodeが7月16日、rork-deviceが7月15日、rork-plistが7月13日に更新されましたOPUS46 — RorkでClaude Opus 4.6が稼働しています。Rork MaxはClaude Codeを土台にアプリを組み立てる設計ですSIM — ブラウザ上で動くクラウドのiOSシミュレータを備え、実機へのインストールは1クリック、App Storeへの提出は2クリックと案内されていますMAX — Rork MaxはReact Nativeではなく純粋なSwiftを出力します。iPhone・iPad・Apple Watch・Apple TV・Vision Pro、そしてiMessageまでが射程ですNATIVE — HealthKit、ARKitとLiDAR、NFC、Dynamic Island、Live Activities、Metalによる3D、Core MLのオンデバイス推論まで扱えますSEED — RorkはLeft Lane Capitalが主導する1,500万ドルのシードラウンドを実施し、Peak XVとa16z Speedrunが参加しました
記事一覧/開発ツール
開発ツール/2026-05-06上級

Rorkが生成したReact Nativeコードを本番品質に育てるリファクタリング実践ガイド

Rorkが生成するReact Nativeコードに潜む6つの技術的負債を特定し、TypeScript型安全化・コンポーネント分離・状態管理・非同期処理・パフォーマンス・テスタビリティの各観点から本番品質へ引き上げるリファクタリング手法を Before/After コードで徹底解説します。

React Native209リファクタリング4TypeScript8本番品質コード品質3状態管理11パフォーマンス最適化3テスト7

Rorkで3日でアプリの原型ができあがったとき、率直に言って感動しました。プロンプトを入力するたびにコンポーネントが生え、スクリーンが組み上がり、「これが動く」という確信がありました。ところが、そのまま2週間後に新機能を追加しようとしたとき、少し詰まりました。コードを開くたびに「どこを触ればいいのか」という感覚が薄れていたのです。

これはRorkへの批判ではありません。AIが生成するコードは「動くことを最優先に最適化されている」という特性があり、それはそれで正しい設計思想です。しかし個人開発でアプリを長く育てていくためには、ある時点で「動くコード」を「育てられるコード」へと変換する作業が必要になります。

累計5,000万ダウンロードのアプリ事業を個人で運営してきた経験から言えば、アプリが長く生き残るかどうかは最初の実装品質よりも「最初のリファクタリングをきちんとやれたか」で決まることが多いです。以下では、Rorkが生成するReact Nativeコードに特有のパターンを具体的に取り上げ、本番品質へ引き上げるための実践的な手法を解説します。

なぜRork生成コードにリファクタリングが必要なのか

Rorkを含むAIアプリビルダーが生成するReact Nativeコードには、開発の初期段階では問題にならないけれど、アプリが成長するにつれて表面化してくる特性があります。実際のコードを見ていくと、次の6つのパターンが繰り返し登場します。

① TypeScript型の省略(any の多用):速度優先のため、型推論が難しい箇所は anyunknown で代替されます。

② スクリーンコンポーネントの肥大化:1つのファイルに200〜400行のJSXが詰め込まれ、UI・ロジック・データ取得が混在します。

useState の乱立:5〜10個の useState が独立して並び、相互に依存関係があるにもかかわらず個別に管理されます。

useEffect 内の危険な非同期パターンuseEffect にasync関数を直接渡す書き方が頻出し、メモリリークの温床になります。

⑤ 再レンダリングの最適化なしmemouseCallbackuseMemo が使われず、親コンポーネントが更新されるたびに子コンポーネントが再描画されます。

⑥ テストを想定しない設計:API呼び出しがコンポーネント内に直接埋め込まれ、モック化や単体テストが困難な構造になっています。

以下では、それぞれを実際のコード例とともに解説します。公式ドキュメントには載っていないRork特有のパターンに注目してください。

Step 1 — TypeScript型安全化:any との決別

Rorkが生成するAPIレスポンスの取り扱いコードは、次のような形になっていることが多いです。

// ❌ Rorkが生成しがちなパターン
const [userData, setUserData] = useState<any>(null);
const [posts, setPosts] = useState<any[]>([]);
 
const fetchData = async () => {
  const res = await fetch('/api/user');
  const data = await res.json(); // data は any
  setUserData(data);
  setPosts(data.posts); // data.posts も any
};

この書き方は動きます。しかし userData.naem とタイポしてもTypeScriptは警告を出しません。3ヶ月後に自分のコードを読み返したとき、data に何が入っているのかを把握するためにAPIドキュメントを開き直す必要が生まれます。

// ✅ リファクタリング後
type User = {
  id: string;
  name: string;
  email: string;
  avatarUrl: string | null;
};
 
type Post = {
  id: string;
  title: string;
  body: string;
  createdAt: string;
  author: Pick<User, 'id' | 'name'>;
};
 
type UserWithPosts = User & {
  posts: Post[];
};
 
const [userData, setUserData] = useState<UserWithPosts | null>(null);
const [posts, setPosts] = useState<Post[]>([]);
 
const fetchData = async (): Promise<void> => {
  const res = await fetch('/api/user');
  if (!res.ok) throw new Error(`HTTP error: ${res.status}`);
  const data: UserWithPosts = await res.json();
  setUserData(data);
  setPosts(data.posts);
};

型を定義するとき、src/types/ フォルダを作成して user.tspost.ts として分離しておくと、複数のスクリーンから再利用できます。Rorkで生成したプロジェクトには最初このフォルダがありませんが、最初に作るべき場所のひとつです。

実践的な発見:Rorkが生成するコードのうち、res.json() の戻り値を型付けしているケースはほぼありません。ここを as UserWithPosts ではなく satisfies UserWithPosts を使うと、型との整合性をコンパイル時に検証できてより安全です(TypeScript 4.9以降)。

Step 2 — 肥大化したScreenコンポーネントを分割する

Rorkが一度に生成するスクリーンコンポーネントは、250〜400行になることがあります。たとえばユーザープロフィールスクリーンであれば、ヘッダー・アバター・統計情報・投稿一覧・フォローボタンが1つのファイルに詰め込まれます。

// ❌ 肥大化したスクリーンの典型例(抜粋)
export default function ProfileScreen() {
  const [user, setUser] = useState<any>(null);
  const [posts, setPosts] = useState<any[]>([]);
  const [isFollowing, setIsFollowing] = useState(false);
  const [followerCount, setFollowerCount] = useState(0);
 
  // ... 多数のuseEffect、ハンドラ関数、スタイル定義 ...
 
  return (
    <ScrollView style={styles.container}>
      {/* ヘッダー: 約50行のJSX */}
      {/* 統計: 約30行のJSX */}
      {/* 投稿一覧: FlatListを含む約80行のJSX */}
      {/* フォローボタン: 約20行のJSX */}
    </ScrollView>
  );
}

これを分割する際、3つの基準を使っています。①再利用できるか(他のスクリーンでも使えるUI)、②独立したロジックを持つか(それ自身でデータを持つか)、③テストしたいか(独立してテストできる単位か)。

// ✅ 分割後の構造
 
// src/components/profile/ProfileHeader.tsx
type ProfileHeaderProps = {
  user: User;
  isFollowing: boolean;
  onFollowToggle: () => void;
};
 
export function ProfileHeader({ user, isFollowing, onFollowToggle }: ProfileHeaderProps) {
  return (
    <View style={styles.header}>
      <Image source={{ uri: user.avatarUrl ?? undefined }} style={styles.avatar} />
      <Text style={styles.name}>{user.name}</Text>
      <FollowButton isFollowing={isFollowing} onPress={onFollowToggle} />
    </View>
  );
}
 
// src/components/profile/ProfileStats.tsx
type ProfileStatsProps = {
  postCount: number;
  followerCount: number;
  followingCount: number;
};
 
export function ProfileStats({ postCount, followerCount, followingCount }: ProfileStatsProps) {
  return (
    <View style={styles.statsRow}>
      <StatItem label="投稿" value={postCount} />
      <StatItem label="フォロワー" value={followerCount} />
      <StatItem label="フォロー中" value={followingCount} />
    </View>
  );
}
 
// src/screens/ProfileScreen.tsx(スリム化後)
export default function ProfileScreen() {
  const { user, posts, isFollowing, stats, handleFollowToggle } = useProfile();
 
  if (!user) return <LoadingScreen />;
 
  return (
    <ScrollView style={styles.container}>
      <ProfileHeader user={user} isFollowing={isFollowing} onFollowToggle={handleFollowToggle} />
      <ProfileStats {...stats} />
      <PostList posts={posts} />
    </ScrollView>
  );
}

useProfile というカスタムフックを作ることで、ロジックとUIを完全に分離できます。このフックはスクリーンコンポーネントとは別にテストできるようになります。

Step 3 — useState の乱立を整理する

Rorkが生成するスクリーンには、次のような useState が並んでいることがよくあります。

// ❌ 独立したuseStateが乱立するパターン
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [data, setData] = useState<Post[] | null>(null);
const [page, setPage] = useState(1);
const [hasMore, setHasMore] = useState(true);

これらは表面上は独立していますが、実際には密接に連動しています。isLoadingtrue のとき errornull であるべきです。error がセットされたとき isLoadingfalse であるべきです。この「暗黙の制約」を5つの useState で管理しようとすると、ある状態更新を忘れたときにバグが生まれます。

// ✅ useReducerで状態を一元管理
 
type FetchState<T> =
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success'; data: T; page: number; hasMore: boolean }
  | { status: 'error'; message: string };
 
type FetchAction<T> =
  | { type: 'FETCH_START' }
  | { type: 'FETCH_SUCCESS'; data: T; hasMore: boolean }
  | { type: 'FETCH_ERROR'; message: string }
  | { type: 'LOAD_MORE' };
 
function fetchReducer<T>(state: FetchState<T>, action: FetchAction<T>): FetchState<T> {
  switch (action.type) {
    case 'FETCH_START':
      return { status: 'loading' };
    case 'FETCH_SUCCESS':
      return { status: 'success', data: action.data, page: 1, hasMore: action.hasMore };
    case 'FETCH_ERROR':
      return { status: 'error', message: action.message };
    case 'LOAD_MORE':
      if (state.status !== 'success') return state;
      return { ...state, page: state.page + 1 };
    default:
      return state;
  }
}

判別共用型(Discriminated Union)を使うと、status の値によって利用可能なフィールドが型で制限されます。state.status === 'success' のブランチ内では、TypeScriptが state.data の存在を保証してくれます。

小規模なアプリでは useState を整理するだけで十分なこともありますが(状態管理パターンの詳細を参照)、状態が4〜5個を超えて相互依存が発生してきたら useReducer への移行を検討する目安になります。

Step 4 — useEffect 内の非同期処理とメモリリーク対策

Rorkが頻繁に生成するパターンに、useEffect に直接 async 関数を渡す書き方があります。

// ❌ useEffectにasync関数を直接渡すパターン(メモリリークの原因)
useEffect(async () => {
  const data = await fetchPosts();
  setPosts(data); // コンポーネントがアンマウント後に呼ばれる可能性がある
}, [userId]);

useEffect のコールバックは非同期関数を直接受け付けません(TypeScriptは警告を出しませんが、実行時に予期しない動作をします)。さらに深刻な問題は、コンポーネントがアンマウントされた後に setPosts が呼ばれると、Reactが「存在しないコンポーネントへの状態更新」として警告を出すことです。

// ✅ クリーンアップ関数を使った安全な書き方
 
useEffect(() => {
  let cancelled = false; // マウント状態のフラグ
 
  const loadPosts = async () => {
    try {
      dispatch({ type: 'FETCH_START' });
      const data = await fetchPosts(userId);
      if (!cancelled) { // アンマウント後は更新しない
        dispatch({ type: 'FETCH_SUCCESS', data, hasMore: data.length === PAGE_SIZE });
      }
    } catch (err) {
      if (!cancelled) {
        dispatch({ type: 'FETCH_ERROR', message: err instanceof Error ? err.message : '読み込みに失敗しました' });
      }
    }
  };
 
  loadPosts();
 
  return () => {
    cancelled = true; // クリーンアップ: アンマウント時にキャンセル
  };
}, [userId]);

AbortController を使ってfetchリクエスト自体をキャンセルする方法もありますが、上記のフラグ方式は理解しやすく、多くのケースで十分機能します。Rorkで生成したコードを本番に持っていく前に、useEffect 内の非同期処理をすべて確認することをお勧めします。useEffectの無限ループなど関連トラブルも合わせてご確認ください。

Step 5 — 不要な再レンダリングを止める

パフォーマンス最適化は「計測してから最適化する」が原則ですが、Rorkが生成するコードにはよく見られる最適化漏れのパターンがあり、リストアプリや頻繁に更新されるスクリーンで顕著に影響が出ます。

// ❌ 親の更新で毎回再レンダリングされるリストアイテム
function PostList({ posts, onLike }: { posts: Post[]; onLike: (id: string) => void }) {
  return (
    <FlatList
      data={posts}
      renderItem={({ item }) => (
        <PostItem post={item} onLike={onLike} /> // onLikeが毎回新しい関数参照になる
      )}
      keyExtractor={(item) => item.id}
    />
  );
}
// ✅ memo + useCallback を使った最適化
 
// PostItemを memo でラップ(propsが変わらない限り再レンダリングしない)
const PostItem = React.memo(function PostItem({
  post,
  onLike,
}: {
  post: Post;
  onLike: (id: string) => void;
}) {
  return (
    <View style={styles.item}>
      <Text style={styles.title}>{post.title}</Text>
      <TouchableOpacity onPress={() => onLike(post.id)}>
        <Text>❤️</Text>
      </TouchableOpacity>
    </View>
  );
});
 
// 親コンポーネントでonLikeをuseCallbackでメモ化
function PostList({ posts, onLike }: { posts: Post[]; onLike: (id: string) => void }) {
  const handleLike = useCallback((id: string) => {
    onLike(id);
  }, [onLike]); // onLikeが変わったときだけ新しい関数を作成
 
  return (
    <FlatList
      data={posts}
      renderItem={({ item }) => (
        <PostItem post={item} onLike={handleLike} />
      )}
      keyExtractor={(item) => item.id}
      // FlatListの追加最適化
      removeClippedSubviews={true}
      maxToRenderPerBatch={10}
      windowSize={5}
    />
  );
}

ただし、memouseCallbackuseMemo はすべて「メモ化のコスト」を伴います。単純なコンポーネントや、頻繁に更新されないデータには不要です。React DevTools の Profiler で実際に再レンダリングが問題になっている箇所を確認してから適用するのが確実です。

Step 6 — テスト可能なアーキテクチャへの変換

Rorkが生成するコードでテストが書きにくい最大の理由は、API呼び出しがコンポーネント内に直接埋め込まれていることです。

// ❌ APIがコンポーネント内に直接埋め込まれているパターン
export default function UserListScreen() {
  const [users, setUsers] = useState<any[]>([]);
 
  useEffect(() => {
    fetch('https://api.example.com/users') // テスト時にモックできない
      .then(res => res.json())
      .then(data => setUsers(data));
  }, []);
  // ...
}
// ✅ API層を分離してテスト可能にする
 
// src/api/users.ts
export async function getUsers(): Promise<User[]> {
  const res = await fetch('https://api.example.com/users');
  if (!res.ok) throw new Error(`Failed to fetch users: ${res.status}`);
  return res.json();
}
 
// src/hooks/useUsers.ts
import { getUsers } from '../api/users';
 
export function useUsers() {
  const [state, dispatch] = useReducer(fetchReducer<User[]>, { status: 'idle' });
 
  const load = useCallback(async () => {
    dispatch({ type: 'FETCH_START' });
    try {
      const data = await getUsers(); // 依存注入で差し替え可能
      dispatch({ type: 'FETCH_SUCCESS', data, hasMore: false });
    } catch (err) {
      dispatch({ type: 'FETCH_ERROR', message: err instanceof Error ? err.message : 'エラー' });
    }
  }, []);
 
  useEffect(() => { load(); }, [load]);
 
  return { state, reload: load };
}
 
// src/screens/UserListScreen.tsx
export default function UserListScreen() {
  const { state } = useUsers();
 
  if (state.status === 'loading') return <ActivityIndicator />;
  if (state.status === 'error') return <ErrorView message={state.message} />;
  if (state.status !== 'success') return null;
 
  return <FlatList data={state.data} renderItem={...} keyExtractor={...} />;
}
// __tests__/useUsers.test.ts — テストが書きやすくなる
import { renderHook, waitFor } from '@testing-library/react-native';
import { useUsers } from '../src/hooks/useUsers';
import * as usersApi from '../src/api/users';
 
jest.mock('../src/api/users');
 
test('ユーザー一覧を正常に取得できる', async () => {
  const mockUsers: User[] = [
    { id: '1', name: 'テストユーザー', email: 'test@example.com', avatarUrl: null },
  ];
  (usersApi.getUsers as jest.Mock).mockResolvedValue(mockUsers);
 
  const { result } = renderHook(() => useUsers());
 
  await waitFor(() => {
    expect(result.current.state.status).toBe('success');
  });
 
  if (result.current.state.status === 'success') {
    expect(result.current.state.data).toEqual(mockUsers);
  }
});

API層・カスタムフック・UIコンポーネントの3層に分離することで、それぞれを独立してテストできるようになります。すべての関数にテストを書く必要はありませんが、ビジネスロジックが集まるカスタムフックのテストを書いておくと、リファクタリング時に変更が安全かどうかを確認できる安心感が生まれます。

リファクタリングの優先順位:何から手をつけるか

6つのステップを一度に全部やろうとすると、リファクタリング自体がプロジェクトを止める原因になります。個人開発では特に、「完璧にしたい」という欲求と「早くリリースしたい」という現実のバランスが難しいところです。

私が実際に使っている優先順位の考え方は、ビジネスリスク × コード変更頻度 のマトリックスです。

今すぐ対処する(リリース前必須)

  • useEffect内のメモリリーク(Step 4):本番でユーザーが遭遇するクラッシュの原因になります
  • APIエラーハンドリングの欠如:ネットワークエラー時にアプリが固まります

1〜2スプリントで対処する(次の機能追加前に)

  • TypeScript型安全化(Step 1):新機能追加のたびにバグが混入しやすい箇所
  • 肥大化スクリーンの分割(Step 2):変更頻度の高いスクリーンから優先

余裕があれば対処する(品質向上)

  • 再レンダリング最適化(Step 5):実際に遅いと感じてから計測して対応
  • テスト(Step 6):コア機能から段階的に追加

この優先順位付けは、Rorkで初めてアプリを作る方にも、既存のRorkプロジェクトを見直している方にも、同じように適用できると思います。完全なコードを目指すよりも、「この部分が変わっても壊れないという確信を持てるコード」を少しずつ増やしていくことが、長く育てられるアプリへの道だと感じています。

全体を振り返って:次の一歩

まず useEffect 内の非同期処理を確認することをお勧めします。既存のRorkプロジェクトを開いて、asyncuseEffect のコールバック内に直接書かれていたら、今日のうちに Step 4 のパターンで書き直してみてください。それだけで本番で遭遇するエラーのかなりの部分を防げます。

その次に TypeScript型を少しずつ充実させていくと、IDE の補完が賢くなり、新機能を追加するスピードが上がります。リファクタリングは一度完了するものではなく、アプリと一緒に少しずつ育てていくものです。

実装の参考になれば幸いです。

シェア

お読みいただきありがとうございます

Rork Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • コピー&ペーストで使える実装コード付き
  • 毎日新しい上級ガイドを追加
  • ¥580/月 または ¥1,480 の永久アクセス
メンバーシップを見る →

もしこの記事がお役に立ちましたら、チップ(¥150)で応援いただけると大変励みになります。広告なしでの運営を続けるため、皆さまのご支援が大きな力になっています。

関連記事

開発ツール2026-07-14
AI再生成に壊されない設計 — Rork の生成コードと手書きロジックを分ける境界の引き方
Rork へ追加指示を出すたびに手書きのロジックが消える。プロンプトで守るのは対症療法です。生成コードと自分の所有コードを層で分け、再生成が届かない境界を設計で引く方法を、Zustand と service 層の実装例つきでお伝えします。
開発ツール2026-07-13
WebViewのpostMessageは投げたら終わり — 相関IDで要求と応答を結ぶブリッジを設計する
WebView と React Native の postMessage は片道通信で、送った処理が成功したのか分かりません。相関IDと応答を組み込んだ要求・応答型ブリッジを、動く TypeScript コードで設計します。
開発ツール2026-06-21
Rork のお気に入り一覧を長押しドラッグで並べ替える — 再レンダリングを抑えた reorderable list と並び順の永続化
Rork が生成したお気に入り一覧に、長押しドラッグの並べ替えを後から足す実装手順です。再レンダリングを抑える設計、worklet の境界、並び順の永続化、ジャンクやゴーストカードの回避までを実コードでまとめました。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます
もっと見る →