01 — Overview
Five Layers, One Direction
Unidirectional data flow from UI to Domain to Data. Each layer knows only the layer below it. UI never touches the database; data layer never imports a React component.
02 — Layer Breakdown
Each Layer Owns Its Job
Boundaries are enforced by TypeScript interfaces and inversion of control — not by convention or team discipline alone.
03 — Features Deep Dive
Every Concern Solved
Speech-to-text, API encryption, offline-first, cancellable calls with debounce, secure token storage, real-time events, and 60fps animations — none of these are bolted on.
import axios, { AxiosInstance, InternalAxiosRequestConfig } from 'axios';
import { secureStorage } from '../storage/secureStorage';
import { encryptionService } from '../../core/security/EncryptionService';
const createHttpClient = (): AxiosInstance => {
const client = axios.create({
baseURL: AppConfig.API_BASE_URL,
timeout: 15_000,
});
// 1. Inject JWT + AES-encrypt body
client.interceptors.request.use(async (config: InternalAxiosRequestConfig) => {
const token = await secureStorage.getAccessToken();
if (token) config.headers.Authorization = `Bearer ${token}`;
if (config.data) {
config.data = await encryptionService.encryptPayload(config.data);
config.headers['X-Encrypted'] = '1';
}
return config;
});
// 2. Decrypt response + handle token refresh
client.interceptors.response.use(
async (response) => {
if (response.headers['x-encrypted']) {
response.data = await encryptionService.decryptPayload(response.data);
}
return response;
},
async (error) => {
if (error.response?.status === 401) {
await refreshTokenAndRetry(error, client);
}
return Promise.reject(toAppError(error));
}
);
return client;
};
// Cancellable request helper
export const cancellableRequest = <T>(
request: (signal: AbortSignal) => Promise<T>
) => {
const controller = new AbortController();
return { promise: request(controller.signal), cancel: () => controller.abort() };
};
export const useSearchQuery = (rawQuery: string) => {
const debouncedQuery = useDebounce(rawQuery, 300);
return useQuery({
queryKey: ['search', debouncedQuery],
queryFn: ({ signal }) => userApiService.search(debouncedQuery, signal),
enabled: debouncedQuery.length > 1, // skip empty / single char
staleTime: 30_000, // 30s — search cache
placeholderData: keepPreviousData, // no flicker between keystrokes
});
};
// useDebounce — typed, cancels on unmount
export const useDebounce = <T>(value: T, delay: number): T => {
const [debounced, setDebounced] = useState<T>(value);
useEffect(() => {
const id = setTimeout(() => setDebounced(value), delay);
return () => clearTimeout(id);
}, [value, delay]);
return debounced;
};
export class UserRepositoryImpl implements IUserRepository {
constructor(
private api: UserApiService,
private db: Database,
) {}
async getUser(id: string): Promise<User> {
// 1. Try local cache first
const local = await this.db.collections
.get<UserModel>('users').find(id);
if (local) return toDomain(local);
// 2. Fetch from network
const remote = await this.api.getUser(id);
// 3. Persist to local DB
await this.db.write(async () => {
await this.db.collections
.get<UserModel>('users')
.create(u => { u._raw.id = remote.id; u.name = remote.name; });
});
return remote;
}
}
// TanStack Query persistence setup
const mmkvPersister = createMMKVStoragePersister({ storage: mmkv });
persistQueryClient({ queryClient, persister: mmkvPersister, maxAge: Infinity });
import * as Keychain from 'react-native-keychain';
const SERVICE = 'com.app.tokens';
export const secureStorage = {
async saveTokens(access: string, refresh: string) {
await Keychain.setGenericPassword(
'tokens',
JSON.stringify({ access, refresh }),
{
service: SERVICE,
accessControl: Keychain.ACCESS_CONTROL.BIOMETRY_ANY_OR_DEVICE_PASSCODE,
accessible: Keychain.ACCESSIBLE.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
}
);
},
async getAccessToken(): Promise<string | null> {
const creds = await Keychain.getGenericPassword({ service: SERVICE });
if (!creds) return null;
return JSON.parse(creds.password).access;
},
async clearTokens() {
await Keychain.resetGenericPassword({ service: SERVICE });
},
};
// MMKV — typed wrapper for non-sensitive values
const storage = new MMKV({ id: 'app-prefs' });
export const prefs = {
setTheme: (mode: 'light' | 'dark') => storage.set('theme', mode),
getTheme: () => storage.getString('theme'),
setOnboarded: () => storage.set('onboarded', true),
isOnboarded: () => storage.getBoolean('onboarded') ?? false,
};
import Aes from 'react-native-aes-crypto';
class EncryptionService {
private sessionKey: string | null = null;
async initSessionKey(serverPublicKey: string) {
// ECDH: derive shared secret, store in Keychain
this.sessionKey = await deriveECDHSecret(serverPublicKey);
await secureStorage.saveKey(this.sessionKey);
}
async encryptPayload(data: unknown): Promise<string> {
const key = await this.getKey();
const iv = await Aes.randomKey(16); // 128-bit IV
const plaintext = JSON.stringify(data);
const cipher = await Aes.encrypt(plaintext, key, iv, 'aes-256-gcm');
// Pack as base64: iv.cipher (server splits on first 32 chars)
return `${iv}.${cipher}`;
}
async decryptPayload(packed: string): Promise<unknown> {
const [iv, cipher] = packed.split('.');
const key = await this.getKey();
const plaintext = await Aes.decrypt(cipher, key, iv, 'aes-256-gcm');
return JSON.parse(plaintext);
}
}
import Voice, { SpeechResultsEvent } from '@react-native-voice/voice';
import * as Speech from 'expo-speech';
export const useSpeech = () => {
const [transcript, setTranscript] = useState('');
const [isListening, setListening] = useState(false);
useEffect(() => {
Voice.onSpeechPartialResults = (e: SpeechResultsEvent) =>
setTranscript(e.value?.[0] ?? '');
Voice.onSpeechResults = (e: SpeechResultsEvent) => {
setTranscript(e.value?.[0] ?? '');
setListening(false);
};
return () => { Voice.destroy().then(Voice.removeAllListeners); };
}, []);
const startListening = async (locale = 'en-US') => {
setTranscript('');
setListening(true);
await Voice.start(locale);
};
const stopListening = async () => {
await Voice.stop();
setListening(false);
};
const speak = (text: string, language = 'en-US') =>
Speech.speak(text, { language, pitch: 1.0, rate: 0.9 });
return { transcript, isListening, startListening, stopListening, speak };
};
import { io, Socket } from 'socket.io-client';
interface ServerToClientEvents {
notification: (payload: { title: string; body: string }) => void;
data_update: (payload: { entity: string; id: string }) => void;
}
let socket: Socket<ServerToClientEvents> | null = null;
export const connectSocket = (token: string) => {
socket = io(AppConfig.WS_URL, {
auth: { token },
reconnection: true,
reconnectionDelay: 1000,
reconnectionDelayMax: 30_000,
reconnectionAttempts: Infinity,
transports: ['websocket'],
});
socket.on('connect', () => useSocketStore.getState().setConnected(true));
socket.on('disconnect', () => useSocketStore.getState().setConnected(false));
socket.on('data_update', ({ entity, id }) => {
queryClient.invalidateQueries({ queryKey: [entity, id] });
});
};
// Hook for component-level subscriptions
export const useWebSocket = <K extends keyof ServerToClientEvents>(
event: K, handler: ServerToClientEvents[K]
) => {
useEffect(() => {
socket?.on(event, handler as any);
return () => { socket?.off(event, handler as any); };
}, [event, handler]);
};
import Animated, {
FadeInDown, FadeOutUp,
useAnimatedStyle, useSharedValue,
withSpring, withTiming, interpolateColor
} from 'react-native-reanimated';
export const AnimatedListItem = ({ item, index }: Props) => {
const pressed = useSharedValue(0);
const animStyle = useAnimatedStyle(() => ({
transform: [{ scale: withSpring(pressed.value ? 0.96 : 1, { damping: 15 }) }],
backgroundColor: interpolateColor(
pressed.value, [0, 1],
['transparent', 'rgba(251,191,36,0.08)']
),
}));
return (
<Animated.View
entering={FadeInDown.delay(index * 40).springify().damping(14)}
exiting={FadeOutUp.duration(200)}
style={animStyle}
>
<Pressable
onPressIn={() => { pressed.value = 1; }}
onPressOut={() => { pressed.value = 0; }}
>
<Text>{item.title}</Text>
</Pressable>
</Animated.View>
);
};
// List screen — tag the shared element
import { SharedElement } from 'react-native-shared-element';
<SharedElement id={`user.avatar.${user.id}`}>
<Image source={{ uri: user.avatarUrl }} style={styles.avatar} />
</SharedElement>
// Detail screen — same id, Reanimated handles the morph
<SharedElement id={`user.avatar.${userId}`}>
<Image source={{ uri: user.avatarUrl }} style={styles.heroBanner} />
</SharedElement>
04 — Request Lifecycle
A Data Request's Journey
From a screen component dispatching an intent to pixels updating on screen — every hop is traceable and testable.
HTTP Request Pipeline
Real-time Event Lifecycle
05 — Principles
SOLID + KISS in React Native
Each principle maps to a concrete decision in this codebase — not a poster on the wall.
06 — Package Registry
The Definitive Stack
No Redux. No AsyncStorage for tokens. No moment.js. No class components. Proven, actively-maintained, TypeScript-first picks only.
✦ — AI Scaffold
Generate with AI
Paste into Claude, ChatGPT, Gemini, or any AI agent to scaffold this exact architecture from scratch.
You are a senior React Native architect. Scaffold a production-ready cross-platform app. Stack: - Framework: React Native 0.74+ (New Architecture enabled) - Language: TypeScript strict mode - Navigation: React Navigation v6 (Stack, Tab, Drawer) - State: Zustand (global) + React Query (server state) - Forms: React Hook Form + Zod validation - Network: Axios + React Query with offline support - Local: MMKV storage + WatermelonDB (complex local data) - Styling: StyleSheet + NativeWind (Tailwind) - Testing: Jest + React Native Testing Library + Detox (E2E) - CI/CD: EAS Build + EAS Submit (Expo Application Services) Provide: 1. Feature-based folder structure (features/, shared/, navigation/, store/) 2. Navigation stack with typed route params 3. React Query setup with global error/loading handling 4. Zustand store with persist middleware (MMKV) 5. Custom hook pattern for API integration 6. Form with React Hook Form + Zod schema validation 7. Jest unit test for a custom hook + mock setup 8. EAS build profile configuration (development/preview/production)