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-03-20中級

Rork 実践テクニック集【前編】— アプリ設計・UI構築・基本的な開発パターン

Rork Lab のプレミアム記事から厳選した実践テクニックの前編。アプリ設計のベストプラクティス、UI構築パターン、Supabase連携、基本的なテストの書き方をコード付きで解説。

Rork515実践テクニック2アプリ設計6UI9Supabase33前編チュートリアル23

取り組みの背景 — この記事シリーズについて

本シリーズ「Rork 実践テクニック集」は、Rork Lab のプレミアム講座から厳選した実装パターンを、実装可能なコード例とともに解説する中級者向けガイドです。

前編(本記事)で学べること:

  • アプリ設計のベストプラクティス
  • UI 構築の効率的なパターン
  • Supabase を使った基本的なバックエンド連携
  • Jest による単体テストの書き方

後編(プレミアム記事)で扱うトピック:

  • StoreKit 2 と RevenueCat による課金実装
  • Detox E2E テストと本番品質の確保
  • パフォーマンス最適化とオフラインファースト設計
  • CI/CD パイプライン(EAS + GitHub Actions)
  • AI 機能の統合(Gemini API + Cloudflare Workers)

このガイドに従えば、プロダクションレディなアプリを Rork で構築できるようになります。


アプリ設計のベストプラクティス

プロンプト設計のコツ(効果的な指示の出し方)

Rork で高品質なアプリを生成するためには、明確で構造化されたプロンプト が不可欠です。

Step 1:アプリの全体像を定義する

以下の仕様のアプリを Rork で構築します。

【アプリ名】
TaskFlow Pro

【概要】
チーム向けのタスク管理アプリ。複数ユーザーでリアルタイムにタスクを共有・更新でき、
プロジェクト管理が容易。

【対応プラットフォーム】
iOS / Android(React Native)

【ターゲットユーザー】
中小企業のプロジェクトマネージャー・チームメンバー

Step 2:主要な画面構成を列挙する

【画面一覧】
1. 認証画面
   - ログイン(メール + パスワード)
   - サインアップ(名前 + メール + パスワード + 企業名)

2. ホーム画面
   - 本日のタスク一覧(優先度順)
   - タスクの完了・未完了をトグル
   - プロジェクトフィルタ

3. タスク詳細画面
   - タイトル・説明・期限・優先度
   - 担当者アサイン
   - コメント機能

4. プロジェクト管理画面
   - プロジェクト一覧
   - メンバー管理
   - 新規プロジェクト作成

Step 3:データモデルを指定する

【データベーススキーマ(Supabase PostgreSQL)】

users テーブル
- id (UUID, primary key)
- email (varchar, unique)
- full_name (varchar)
- company_name (varchar)
- created_at (timestamp)

projects テーブル
- id (UUID, primary key)
- user_id (UUID, foreign key)
- name (varchar)
- description (text)
- status (enum: 'active', 'archived')
- created_at (timestamp)

tasks テーブル
- id (UUID, primary key)
- project_id (UUID, foreign key)
- assigned_to (UUID, foreign key users)
- title (varchar)
- description (text)
- priority (enum: 'low', 'medium', 'high')
- status (enum: 'todo', 'in_progress', 'done')
- due_date (date)
- created_at (timestamp)
- updated_at (timestamp)

comments テーブル
- id (UUID, primary key)
- task_id (UUID, foreign key)
- user_id (UUID, foreign key)
- content (text)
- created_at (timestamp)

Step 4:認証とセキュリティを指定する

【認証・セキュリティ】
- 認証:Supabase Auth(メール + パスワード)
- RLS(Row Level Security):有効化
  * users テーブル:自分のレコードのみ読み書き可能
  * projects テーブル:プロジェクトメンバーのみアクセス可能
  * tasks テーブル:割り当てられたユーザーと作成者がアクセス可能
- Session 管理:React Native AsyncStorage で JWT 管理

効果的なプロンプトを書くことで、Rork AI は自動的に上記のすべての要件を満たすコードを生成します。

コンポーネント分割のパターン

大規模アプリの場合、コンポーネント設計が保守性の鍵になります。

推奨フォルダ構成:

src/
├── components/
│   ├── common/
│   │   ├── Button.jsx
│   │   ├── Card.jsx
│   │   ├── Modal.jsx
│   │   └── LoadingSpinner.jsx
│   ├── auth/
│   │   ├── LoginScreen.jsx
│   │   └── SignupScreen.jsx
│   ├── tasks/
│   │   ├── TaskList.jsx
│   │   ├── TaskCard.jsx
│   │   └── TaskDetailsModal.jsx
│   └── projects/
│       ├── ProjectList.jsx
│       ├── ProjectCard.jsx
│       └── ProjectMembersModal.jsx
├── hooks/
│   ├── useAuth.js
│   ├── useSupabase.js
│   └── useTasks.js
├── context/
│   ├── AuthContext.jsx
│   └── TaskContext.jsx
├── utils/
│   ├── supabaseClient.js
│   ├── validation.js
│   └── dateFormatter.js
└── navigation/
    └── RootNavigator.jsx

Custom Hook の例(useAuth):

import { useState, useEffect, useCallback } from 'react';
import { supabase } from '../utils/supabaseClient';
 
export const useAuth = () => {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
 
  useEffect(() => {
    const session = supabase.auth.session();
    setUser(session?.user ?? null);
 
    const { subscription } = supabase.auth.onAuthStateChange(
      (event, session) => {
        setUser(session?.user ?? null);
        setLoading(false);
      }
    );
 
    return () => {
      subscription?.unsubscribe();
    };
  }, []);
 
  const login = useCallback(async (email, password) => {
    setLoading(true);
    setError(null);
    try {
      const { data, error } = await supabase.auth.signInWithPassword({
        email,
        password,
      });
      if (error) throw error;
      setUser(data.user);
    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  }, []);
 
  const logout = useCallback(async () => {
    setLoading(true);
    try {
      const { error } = await supabase.auth.signOut();
      if (error) throw error;
      setUser(null);
    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  }, []);
 
  return { user, loading, error, login, logout };
};

ナビゲーション設計(Tab・Stack・Drawer)

React Navigation の構造化は、ユーザーフロー改善に直結します。

推奨パターン:Tab + Stack ハイブリッド

import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { Ionicons } from '@expo/vector-icons';
 
import HomeScreen from '../screens/HomeScreen';
import ProjectsScreen from '../screens/ProjectsScreen';
import ProfileScreen from '../screens/ProfileScreen';
import TaskDetailsScreen from '../screens/TaskDetailsScreen';
 
const Stack = createNativeStackNavigator();
const Tab = createBottomTabNavigator();
 
const HomeStack = () => {
  return (
    <Stack.Navigator
      screenOptions={{
        headerShown: true,
        headerStyle: { backgroundColor: '#0066cc' },
        headerTintColor: '#fff',
        headerTitleStyle: { fontWeight: '600' },
      }}
    >
      <Stack.Screen
        name="HomeList"
        component={HomeScreen}
        options={{ title: 'ホーム' }}
      />
      <Stack.Screen
        name="TaskDetails"
        component={TaskDetailsScreen}
        options={{ title: 'タスク詳細' }}
      />
    </Stack.Navigator>
  );
};
 
const ProjectsStack = () => {
  return (
    <Stack.Navigator
      screenOptions={{
        headerShown: true,
        headerStyle: { backgroundColor: '#0066cc' },
        headerTintColor: '#fff',
      }}
    >
      <Stack.Screen
        name="ProjectsList"
        component={ProjectsScreen}
        options={{ title: 'プロジェクト' }}
      />
      <Stack.Screen
        name="ProjectDetails"
        component={ProjectDetailsScreen}
        options={{ title: 'プロジェクト詳細' }}
      />
    </Stack.Navigator>
  );
};
 
const RootNavigator = () => {
  return (
    <NavigationContainer>
      <Tab.Navigator
        screenOptions={({ route }) => ({
          headerShown: false,
          tabBarIcon: ({ focused, color, size }) => {
            let iconName;
            if (route.name === 'Home') {
              iconName = focused ? 'home' : 'home-outline';
            } else if (route.name === 'Projects') {
              iconName = focused ? 'folder' : 'folder-outline';
            } else if (route.name === 'Profile') {
              iconName = focused ? 'person' : 'person-outline';
            }
            return <Ionicons name={iconName} size={size} color={color} />;
          },
          tabBarActiveTintColor: '#0066cc',
          tabBarInactiveTintColor: '#999',
        })}
      >
        <Tab.Screen
          name="Home"
          component={HomeStack}
          options={{ title: 'ホーム' }}
        />
        <Tab.Screen
          name="Projects"
          component={ProjectsStack}
          options={{ title: 'プロジェクト' }}
        />
        <Tab.Screen
          name="Profile"
          component={ProfileScreen}
          options={{ title: 'プロフィール' }}
        />
      </Tab.Navigator>
    </NavigationContainer>
  );
};
 
export default RootNavigator;

UI 構築の実践パターン

FlatList の効率的な使い方(React Native code)

大規模なリストを扱う場合、FlatList の最適化が必須です。

基本パターン(仮想化・メモ化):

import React, { useMemo, useCallback } from 'react';
import { FlatList, View, Text, StyleSheet, TouchableOpacity } from 'react-native';
 
const TaskListScreen = ({ tasks, onTaskPress, onCompleteTask }) => {
  // タスクを優先度でソート(useMemo で再計算を防止)
  const sortedTasks = useMemo(() => {
    const priorityMap = { high: 1, medium: 2, low: 3 };
    return [...tasks].sort(
      (a, b) => priorityMap[a.priority] - priorityMap[b.priority]
    );
  }, [tasks]);
 
  // キー生成関数
  const keyExtractor = useCallback((item) => item.id, []);
 
  // リスト項目コンポーネント(React.memo で再レンダー防止)
  const TaskItem = React.memo(({ item }) => (
    <TouchableOpacity
      style={styles.taskItem}
      onPress={() => onTaskPress(item.id)}
      activeOpacity={0.7}
    >
      <View style={styles.taskContent}>
        <Text style={styles.taskTitle} numberOfLines={1}>
          {item.title}
        </Text>
        <Text style={styles.taskDescription} numberOfLines={2}>
          {item.description}
        </Text>
      </View>
      <View style={[styles.priorityBadge, styles[`priority${item.priority}`]]}>
        <Text style={styles.priorityText}>
          {item.priority === 'high' ? '高' : item.priority === 'medium' ? '中' : '低'}
        </Text>
      </View>
    </TouchableOpacity>
  ));
 
  return (
    <FlatList
      data={sortedTasks}
      renderItem={({ item }) => <TaskItem item={item} />}
      keyExtractor={keyExtractor}
      initialNumToRender={10}
      maxToRenderPerBatch={10}
      updateCellsBatchingPeriod={50}
      removeClippedSubviews={true}
      contentContainerStyle={styles.container}
    />
  );
};
 
const styles = StyleSheet.create({
  container: {
    paddingVertical: 8,
  },
  taskItem: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
    paddingHorizontal: 16,
    paddingVertical: 12,
    borderBottomWidth: 1,
    borderBottomColor: '#e0e0e0',
  },
  taskContent: {
    flex: 1,
  },
  taskTitle: {
    fontSize: 16,
    fontWeight: '600',
    color: '#333',
    marginBottom: 4,
  },
  taskDescription: {
    fontSize: 13,
    color: '#999',
  },
  priorityBadge: {
    paddingVertical: 4,
    paddingHorizontal: 8,
    borderRadius: 4,
    marginLeft: 12,
  },
  priorityhigh: {
    backgroundColor: '#ffcccc',
  },
  prioritymedium: {
    backgroundColor: '#fff4cc',
  },
  prioritylow: {
    backgroundColor: '#ccf0ff',
  },
  priorityText: {
    fontSize: 12,
    fontWeight: '600',
    color: '#333',
  },
});
 
export default TaskListScreen;

アクセシビリティ対応の基本(VoiceOver・TalkBack)

アプリの使いやすさを障害者も含めた全員に提供することは、今や標準です。

アクセシビリティ対応のプラクティス:

import React from 'react';
import {
  View,
  Text,
  TouchableOpacity,
  StyleSheet,
  AccessibilityInfo,
  useAccessibilityInfo,
} from 'react-native';
 
const AccessibleButton = ({
  title,
  onPress,
  accessibilityLabel,
  accessibilityHint,
  disabled = false,
}) => {
  return (
    <TouchableOpacity
      onPress={onPress}
      disabled={disabled}
      accessible={true}
      accessibilityLabel={accessibilityLabel || title}
      accessibilityHint={accessibilityHint}
      accessibilityRole="button"
      accessibilityState={{ disabled }}
      style={[styles.button, disabled && styles.buttonDisabled]}
    >
      <Text style={[styles.buttonText, disabled && styles.buttonTextDisabled]}>
        {title}
      </Text>
    </TouchableOpacity>
  );
};
 
const AccessibleTaskCard = ({ task, onPress }) => {
  return (
    <TouchableOpacity
      onPress={onPress}
      accessible={true}
      accessibilityLabel={`タスク: ${task.title}`}
      accessibilityHint={`優先度: ${task.priority}, 期限: ${task.dueDate}`}
      accessibilityRole="button"
      activeOpacity={0.7}
      style={styles.card}
    >
      <View>
        <Text
          style={styles.cardTitle}
          allowFontScaling={true}
          maxFontSizeMultiplier={1.5}
        >
          {task.title}
        </Text>
        <Text
          style={styles.cardDescription}
          allowFontScaling={true}
          maxFontSizeMultiplier={1.3}
        >
          {task.description}
        </Text>
      </View>
    </TouchableOpacity>
  );
};
 
const styles = StyleSheet.create({
  button: {
    paddingVertical: 12,
    paddingHorizontal: 24,
    backgroundColor: '#0066cc',
    borderRadius: 8,
    justifyContent: 'center',
    alignItems: 'center',
    minHeight: 44, // iOS アクセシビリティ最小タップ領域
  },
  buttonDisabled: {
    opacity: 0.5,
  },
  buttonText: {
    color: '#fff',
    fontSize: 16,
    fontWeight: '600',
  },
  buttonTextDisabled: {
    color: '#ccc',
  },
  card: {
    padding: 16,
    backgroundColor: '#fff',
    borderRadius: 8,
    marginVertical: 8,
    marginHorizontal: 16,
    minHeight: 56, // タップ領域確保
  },
  cardTitle: {
    fontSize: 18,
    fontWeight: '700',
    color: '#333',
    marginBottom: 8,
  },
  cardDescription: {
    fontSize: 14,
    color: '#666',
    lineHeight: 20,
  },
});
 
export default { AccessibleButton, AccessibleTaskCard };

デザイン品質を上げるTips(配色・タイポグラフィ)

カラーシステムの確立:

// colors.js - 一元管理
export const colors = {
  // プライマリカラー(CTA・重要要素)
  primary: '#0066cc',
  primaryDark: '#004fa3',
  primaryLight: '#e6f0ff',
 
  // セカンダリカラー(情報・補助)
  secondary: '#ff6b35',
  secondaryDark: '#cc5429',
  secondaryLight: '#ffe6d9',
 
  // ニュートラル(テキスト・背景)
  text: {
    primary: '#1a1a1a',
    secondary: '#666666',
    tertiary: '#999999',
    disabled: '#cccccc',
  },
  background: {
    primary: '#ffffff',
    secondary: '#f5f5f5',
    tertiary: '#eeeeee',
  },
  border: '#e0e0e0',
 
  // ステータスカラー
  success: '#22c55e',
  warning: '#f59e0b',
  error: '#ef4444',
  info: '#3b82f6',
};
 
// typography.js - フォント規格
export const typography = {
  h1: {
    fontSize: 32,
    fontWeight: '700',
    lineHeight: 40,
    letterSpacing: -0.5,
  },
  h2: {
    fontSize: 24,
    fontWeight: '700',
    lineHeight: 32,
    letterSpacing: -0.3,
  },
  h3: {
    fontSize: 18,
    fontWeight: '600',
    lineHeight: 24,
  },
  body: {
    fontSize: 16,
    fontWeight: '400',
    lineHeight: 24,
  },
  bodySmall: {
    fontSize: 14,
    fontWeight: '400',
    lineHeight: 20,
  },
  caption: {
    fontSize: 12,
    fontWeight: '400',
    lineHeight: 16,
  },
  label: {
    fontSize: 12,
    fontWeight: '600',
    lineHeight: 16,
    letterSpacing: 0.5,
  },
};

バックエンド連携の基礎

Supabase 認証 + データベースの基本構成(TypeScript code)

Supabase クライアント初期化:

import { createClient } from '@supabase/supabase-js';
import AsyncStorage from '@react-native-async-storage/async-storage';
 
const supabaseUrl = process.env.SUPABASE_URL;
const supabaseAnonKey = process.env.SUPABASE_ANON_KEY;
 
export const supabase = createClient(supabaseUrl, supabaseAnonKey, {
  auth: {
    storage: AsyncStorage,
    autoRefreshToken: true,
    persistSession: true,
    detectSessionInUrl: false,
  },
});

認証サービスの実装:

import { supabase } from './supabaseClient';
 
interface SignUpPayload {
  email: string;
  password: string;
  fullName: string;
  companyName: string;
}
 
interface TaskInsertPayload {
  projectId: string;
  title: string;
  description: string;
  priority: 'low' | 'medium' | 'high';
  dueDate: string;
  assignedTo?: string;
}
 
export const authService = {
  async signUp(payload: SignUpPayload) {
    // Supabase Auth でユーザー作成
    const { data, error } = await supabase.auth.signUp({
      email: payload.email,
      password: payload.password,
    });
 
    if (error) throw error;
 
    // profiles テーブルにプロフィール情報を挿入
    const { error: profileError } = await supabase
      .from('profiles')
      .insert({
        id: data.user.id,
        full_name: payload.fullName,
        company_name: payload.companyName,
      });
 
    if (profileError) throw profileError;
 
    return data;
  },
 
  async signIn(email: string, password: string) {
    const { data, error } = await supabase.auth.signInWithPassword({
      email,
      password,
    });
 
    if (error) throw error;
    return data;
  },
 
  async signOut() {
    const { error } = await supabase.auth.signOut();
    if (error) throw error;
  },
 
  async getCurrentUser() {
    const { data, error } = await supabase.auth.getUser();
    if (error) throw error;
    return data.user;
  },
};
 
export const taskService = {
  async getTasks(projectId: string) {
    const { data, error } = await supabase
      .from('tasks')
      .select('*')
      .eq('project_id', projectId)
      .order('created_at', { ascending: false });
 
    if (error) throw error;
    return data;
  },
 
  async createTask(payload: TaskInsertPayload) {
    const { data, error } = await supabase
      .from('tasks')
      .insert({
        project_id: payload.projectId,
        title: payload.title,
        description: payload.description,
        priority: payload.priority,
        due_date: payload.dueDate,
        assigned_to: payload.assignedTo,
        status: 'todo',
      })
      .select()
      .single();
 
    if (error) throw error;
    return data;
  },
 
  async updateTask(
    taskId: string,
    updates: Partial<{
      title: string;
      status: 'todo' | 'in_progress' | 'done';
      priority: 'low' | 'medium' | 'high';
      due_date: string;
    }>
  ) {
    const { data, error } = await supabase
      .from('tasks')
      .update(updates)
      .eq('id', taskId)
      .select()
      .single();
 
    if (error) throw error;
    return data;
  },
 
  async deleteTask(taskId: string) {
    const { error } = await supabase
      .from('tasks')
      .delete()
      .eq('id', taskId);
 
    if (error) throw error;
  },
};

プッシュ通知の実装基礎

Firebase Cloud Messaging(FCM)の設定:

import { initializeApp } from 'firebase/app';
import {
  getMessaging,
  getToken,
  onMessage,
  Messaging,
} from 'firebase/messaging';
 
const firebaseConfig = {
  apiKey: process.env.FIREBASE_API_KEY,
  projectId: process.env.FIREBASE_PROJECT_ID,
  messagingSenderId: process.env.FIREBASE_MESSAGING_SENDER_ID,
};
 
const app = initializeApp(firebaseConfig);
const messaging = getMessaging(app);
 
export const notificationService = {
  async initializeNotifications() {
    try {
      // FCM トークンを取得
      const token = await getToken(messaging, {
        vapidKey: process.env.FIREBASE_VAPID_KEY,
      });
 
      if (token) {
        console.log('FCM Token:', token);
        // トークンをサーバーに保存して、ユーザーにプッシュ通知を送信可能にする
        await this.saveFcmToken(token);
      }
    } catch (error) {
      console.error('FCM token error:', error);
    }
  },
 
  onMessageListener() {
    return new Promise((resolve) => {
      onMessage(messaging, (payload) => {
        console.log('Message received:', payload);
        // フォアグラウンド時の通知処理
        resolve(payload);
      });
    });
  },
 
  async saveFcmToken(token: string) {
    const user = await authService.getCurrentUser();
    if (!user) return;
 
    await supabase
      .from('user_fcm_tokens')
      .insert({
        user_id: user.id,
        token,
        device_type: 'mobile',
      });
  },
};
 
// 起動時に初期化
notificationService.initializeNotifications();
 
// フォアグラウンド時の通知受信
notificationService.onMessageListener().then((payload) => {
  // UI 表示処理など
});

ディープリンクの基本設定

React Navigation のディープリンク設定:

import { useLinkBuilder, useRoute } from '@react-navigation/native';
import * as Linking from 'expo-linking';
 
const prefix = Linking.createURL('/');
 
const linking = {
  prefixes: [prefix, 'https://yourapp.com', 'yourapp://'],
  config: {
    screens: {
      Home: '',
      TaskDetails: 'task/:taskId',
      ProjectDetails: 'project/:projectId',
      UserProfile: 'profile/:userId',
    },
  },
};
 
export const RootNavigator = () => {
  return (
    <NavigationContainer linking={linking} fallback={<SplashScreen />}>
      {/* ナビゲータ定義 */}
    </NavigationContainer>
  );
};
 
// ディープリンクを処理するカスタムフック
export const useDeepLinkHandler = () => {
  const route = useRoute();
 
  useEffect(() => {
    const handleUrl = ({ url }: { url: string }) => {
      const route = url.replace(/.*?:\/\//g, '');
      const routeName = route.split('/')[0];
      const params = route.split('/').slice(1);
 
      if (routeName === 'task' && params[0]) {
        // タスク詳細画面へ遷移
        navigation.navigate('TaskDetails', { taskId: params[0] });
      } else if (routeName === 'project' && params[0]) {
        // プロジェクト詳細画面へ遷移
        navigation.navigate('ProjectDetails', { projectId: params[0] });
      }
    };
 
    const subscription = Linking.addEventListener('url', handleUrl);
 
    return () => {
      subscription.remove();
    };
  }, []);
};

テストの基礎

Jest 単体テストの書き方(code)

テスト環境の設定:

// jest.config.js
module.exports = {
  preset: 'jest-expo',
  testEnvironment: 'node',
  collectCoverageFrom: [
    'src/**/*.{js,jsx,ts,tsx}',
    '!src/**/*.d.ts',
    '!src/**/index.ts',
  ],
  coverageThreshold: {
    global: {
      branches: 70,
      functions: 70,
      lines: 70,
      statements: 70,
    },
  },
};

ユーティリティ関数のテスト:

// utils/validation.test.js
import { validateEmail, validatePassword } from './validation';
 
describe('validation utilities', () => {
  describe('validateEmail', () => {
    test('should return true for valid email', () => {
      expect(validateEmail('user@example.com')).toBe(true);
    });
 
    test('should return false for invalid email', () => {
      expect(validateEmail('invalid-email')).toBe(false);
      expect(validateEmail('user@')).toBe(false);
      expect(validateEmail('')).toBe(false);
    });
  });
 
  describe('validatePassword', () => {
    test('should return true for strong password', () => {
      expect(validatePassword('Secure123!Pass')).toBe(true);
    });
 
    test('should return false for weak password', () => {
      expect(validatePassword('weak')).toBe(false);
      expect(validatePassword('12345678')).toBe(false);
      expect(validatePassword('NoNumber!')).toBe(false);
    });
  });
});

非同期テスト(API 呼び出し):

// services/taskService.test.js
import { taskService } from './taskService';
import { supabase } from '../utils/supabaseClient';
 
jest.mock('../utils/supabaseClient');
 
describe('taskService', () => {
  describe('getTasks', () => {
    test('should fetch tasks from Supabase', async () => {
      const mockTasks = [
        { id: '1', title: 'Task 1', status: 'todo' },
        { id: '2', title: 'Task 2', status: 'in_progress' },
      ];
 
      supabase.from.mockReturnValue({
        select: jest.fn().mockReturnThis(),
        eq: jest.fn().mockReturnThis(),
        order: jest.fn().mockResolvedValue({ data: mockTasks, error: null }),
      });
 
      const tasks = await taskService.getTasks('project-1');
 
      expect(tasks).toEqual(mockTasks);
      expect(supabase.from).toHaveBeenCalledWith('tasks');
    });
 
    test('should handle Supabase errors', async () => {
      const mockError = new Error('Network error');
 
      supabase.from.mockReturnValue({
        select: jest.fn().mockReturnThis(),
        eq: jest.fn().mockReturnThis(),
        order: jest
          .fn()
          .mockResolvedValue({ data: null, error: mockError }),
      });
 
      await expect(taskService.getTasks('project-1')).rejects.toThrow(
        'Network error'
      );
    });
  });
});

React Native Testing Library の基本

コンポーネントテストの例:

// components/tasks/TaskCard.test.js
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react-native';
import TaskCard from './TaskCard';
 
describe('TaskCard', () => {
  const mockTask = {
    id: '1',
    title: 'Test Task',
    description: 'Task description',
    priority: 'high',
    dueDate: '2026-03-25',
  };
 
  test('should render task title and description', () => {
    render(<TaskCard task={mockTask} onPress={jest.fn()} />);
 
    expect(screen.getByText('Test Task')).toBeTruthy();
    expect(screen.getByText('Task description')).toBeTruthy();
  });
 
  test('should call onPress when card is tapped', () => {
    const mockOnPress = jest.fn();
    render(<TaskCard task={mockTask} onPress={mockOnPress} />);
 
    const taskButton = screen.getByRole('button');
    fireEvent.press(taskButton);
 
    expect(mockOnPress).toHaveBeenCalledWith(mockTask.id);
  });
 
  test('should display high priority badge', () => {
    render(<TaskCard task={mockTask} onPress={jest.fn()} />);
 
    expect(screen.getByText('高')).toBeTruthy();
  });
});

次のステップ — 後編(プレミアム)で学べること

このガイドで、Rork での基本的なアプリ設計と実装パターンを習得しました。

次のステップでは、アプリをプロダクションレディにするための高度なテクニックを学びます。

【Rork 実践テクニック集【後編】】で扱うトピック:

  1. アプリ収益化 — StoreKit 2、RevenueCat、AdMob の統合
  2. 本番品質の確保 — Detox E2E テスト、Sentry クラッシュ解析
  3. CI/CD パイプライン — EAS Build/Update + GitHub Actions
  4. パフォーマンス最適化 — メモリ管理、オフラインファースト設計
  5. AI 機能統合 — Gemini API ストリーミング、Cloudflare Workers
  6. App Store 成長戦略 — ASO、マーケティング

これらを習得すれば、Rork で数百万ダウンロードを目指すアプリが構築できるようになります。


記事情報

  • 公開日:2026年3月20日
  • 対応バージョン:Rork v3.2.0 以上、React Native 0.73.0 以上
  • 関連記事
    • 「Rork 完全ガイド 2026」(初級向け)
    • 「Rork 実践テクニック集【後編】」(プレミアム・上級向け)

ご質問は Rork Lab Discord コミュニティまで。

シェア

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

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

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

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

関連記事

開発ツール2026-07-03
App Store Connect API の日次集計が月末に合わなくなるとき — JWT 失効・レポート確定遅延・突合の運用メモ
App Store Connect API で自動化した売上・レビュー収集は、止まらずに「欠け」ます。401/404/429 が生む3種類の静かな欠損を fetch ログと突合クエリで検出し、バックフィルで埋める運用設計を実装コード付きでまとめます。
開発ツール2026-06-23
ゲーミフィケーションでDAUは増えたのに継続率が動かなかった — Rork運用で効いた設計の立て直し
ポイント・バッジ・リーダーボードを入れるとDAUは上がりますが、継続率は別物です。サーバー権威のポイント台帳、燃え尽きないストリーク、新規を萎えさせないリーグ設計まで、Rork運用で効いた立て直しを実装コード付きで整理します。
開発ツール2026-05-05
Rork でリアルタイム為替換算アプリを作る — API 連携・ホーム画面ウィジェット・エラー対策まで
Rork を使って為替換算アプリをゼロから作るチュートリアル。ExchangeRate-API との連携、ホーム画面ウィジェット対応、よくあるエラーの対処法まで実践的に解説します。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます
もっと見る →