Setup and context — About This Article Series
This "Rork Practical Techniques" series extracts implementation patterns from Rork Lab premium courses and presents working code examples for intermediate developers.
Part 1 Topics:
- App design best practices
- Efficient UI building patterns
- Basic Supabase backend integration
- Jest unit testing
Part 2 Topics (Premium):
- StoreKit 2 and RevenueCat monetization
- Detox E2E testing and production quality
- Performance optimization and offline-first design
- CI/CD pipelines (EAS + GitHub Actions)
- AI feature integration (Gemini API + Cloudflare Workers)
Follow this guide to build production-ready apps with Rork.
App Design Best Practices
Effective Prompt Design (Precise Instructions)
High-quality apps from Rork require clear, structured prompts.
Step 1: Define the Overall Vision
Building this app with Rork:
【App Name】
TaskFlow Pro
【Overview】
Team task management. Multiple users share and update tasks in real-time.
Easy project management.
【Platform】
iOS / Android (React Native)
【Target Users】
Small business project managers and team members
Step 2: List Main Screens
【Screen List】
1. Authentication Screen
- Login (email + password)
- Signup (name + email + password + company)
2. Home Screen
- Today's task list (by priority)
- Toggle task complete/incomplete
- Project filter
3. Task Details Screen
- Title, description, due date, priority
- Assign to team member
- Comments
4. Project Management Screen
- Project list
- Member management
- Create new project
Step 3: Specify Data Models
【Database Schema (Supabase PostgreSQL)】
users table
- id (UUID, primary key)
- email (varchar, unique)
- full_name (varchar)
- company_name (varchar)
- created_at (timestamp)
projects table
- id (UUID, primary key)
- user_id (UUID, foreign key)
- name (varchar)
- description (text)
- status (enum: 'active', 'archived')
- created_at (timestamp)
tasks table
- 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 table
- id (UUID, primary key)
- task_id (UUID, foreign key)
- user_id (UUID, foreign key)
- content (text)
- created_at (timestamp)
Step 4: Specify Auth and Security
【Authentication & Security】
- Auth: Supabase Auth (email + password)
- RLS (Row Level Security): Enabled
* users table: Read/write own records only
* projects table: Members only
* tasks table: Assignee and creator access
- Session: React Native AsyncStorage for JWT
Clear prompts allow Rork AI to auto-generate code meeting all requirements.
Component Splitting Patterns
Large apps need solid component architecture.
Recommended Folder Structure:
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 Example (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 };
};Navigation Design (Tab, Stack, Drawer)
Solid navigation structure improves UX.
Recommended Pattern: Tab + Stack Hybrid
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: 'Home' }}
/>
<Stack.Screen
name="TaskDetails"
component={TaskDetailsScreen}
options={{ title: 'Task Details' }}
/>
</Stack.Navigator>
);
};
const ProjectsStack = () => {
return (
<Stack.Navigator
screenOptions={{
headerShown: true,
headerStyle: { backgroundColor: '#0066cc' },
headerTintColor: '#fff',
}}
>
<Stack.Screen
name="ProjectsList"
component={ProjectsScreen}
options={{ title: 'Projects' }}
/>
<Stack.Screen
name="ProjectDetails"
component={ProjectDetailsScreen}
options={{ title: 'Project Details' }}
/>
</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: 'Home' }}
/>
<Tab.Screen
name="Projects"
component={ProjectsStack}
options={{ title: 'Projects' }}
/>
<Tab.Screen
name="Profile"
component={ProfileScreen}
options={{ title: 'Profile' }}
/>
</Tab.Navigator>
</NavigationContainer>
);
};
export default RootNavigator;UI Building in Practice
Efficient FlatList Usage (Virtualization & Memoization)
Large lists require optimization.
Core Pattern (virtualization + memoization):
import React, { useMemo, useCallback } from 'react';
import { FlatList, View, Text, StyleSheet, TouchableOpacity } from 'react-native';
const TaskListScreen = ({ tasks, onTaskPress, onCompleteTask }) => {
// Sort by priority (useMemo prevents recalculation)
const sortedTasks = useMemo(() => {
const priorityMap = { high: 1, medium: 2, low: 3 };
return [...tasks].sort(
(a, b) => priorityMap[a.priority] - priorityMap[b.priority]
);
}, [tasks]);
// Key extraction
const keyExtractor = useCallback((item) => item.id, []);
// Item component (React.memo prevents re-render)
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' ? 'High' : item.priority === 'medium' ? 'Med' : 'Low'}
</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;Accessibility Basics (VoiceOver / TalkBack)
Modern apps support everyone including people with disabilities.
Accessibility Best Practices:
import React from 'react';
import {
View,
Text,
TouchableOpacity,
StyleSheet,
AccessibilityInfo,
} 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: ${task.title}`}
accessibilityHint={`Priority: ${task.priority}, Due: ${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 accessibility minimum
},
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 };Design Quality Tips (Color System, Typography)
Establish a Color System:
// colors.js
export const colors = {
// Primary (CTAs, important elements)
primary: '#0066cc',
primaryDark: '#004fa3',
primaryLight: '#e6f0ff',
// Secondary (info, assistance)
secondary: '#ff6b35',
secondaryDark: '#cc5429',
secondaryLight: '#ffe6d9',
// Neutral (text, background)
text: {
primary: '#1a1a1a',
secondary: '#666666',
tertiary: '#999999',
disabled: '#cccccc',
},
background: {
primary: '#ffffff',
secondary: '#f5f5f5',
tertiary: '#eeeeee',
},
border: '#e0e0e0',
// Status
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,
},
};Backend Integration Essentials
Supabase Auth + Database Basics (TypeScript)
Initialize Supabase Client:
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,
},
});Auth Service Implementation:
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) {
const { data, error } = await supabase.auth.signUp({
email: payload.email,
password: payload.password,
});
if (error) throw error;
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;
},
};Push Notifications Basics
Firebase Cloud Messaging Setup:
import { initializeApp } from 'firebase/app';
import {
getMessaging,
getToken,
onMessage,
} 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 {
const token = await getToken(messaging, {
vapidKey: process.env.FIREBASE_VAPID_KEY,
});
if (token) {
console.log('FCM Token:', token);
// Save to server for push notifications
await this.saveFcmToken(token);
}
} catch (error) {
console.error('FCM 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',
});
},
};
// Initialize on startup
notificationService.initializeNotifications();Deep Linking Basics
React Navigation Deep Link Configuration:
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 />}>
{/* Navigator definition */}
</NavigationContainer>
);
};
// Deep link handler hook
export const useDeepLinkHandler = () => {
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();
};
}, []);
};Testing Fundamentals
Jest Unit Testing
Test Setup:
// 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,
},
},
};Utility Function Test:
// 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 Test with Mocking:
// 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'
);
});
});
});Component Testing with React Native Testing Library
Component Test Example:
// 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('High')).toBeTruthy();
});
});Next Steps — Part 2 (Premium Topics)
Part 1 covered design and implementation fundamentals. Part 2 covers:
- App Monetization — StoreKit 2, RevenueCat, AdMob integration
- Production Quality — Detox E2E testing, Sentry crash analytics
- CI/CD Pipeline — EAS Build/Update + GitHub Actions
- Performance Optimization — Memory management, offline-first design
- AI Integration — Gemini API streaming, Cloudflare Workers
- App Store Strategy — ASO, marketing
Complete Part 2 and you'll build apps targeting millions of downloads.
Article Info
- Published: March 20, 2026
- Compatible With: Rork v3.2.0+, React Native 0.73.0+
- Related Articles
- "Rork Complete Guide 2026" (beginner)
- "Rork Practical Techniques [Part 2]" (premium, advanced)
Questions? Visit Rork Lab Discord community.