name: arcana-react-developer-skill description: React development guide based on Arcana React enterprise architecture. Provides comprehensive support for Clean Architecture, Offline-First design with 4-layer caching, React 19 hooks, MVVM Input/Output/Effect pattern, and enterprise security. Suitable for React project development, architecture design, code review, and debugging. allowed-tools: [Read, Grep, Glob, Bash, Write, Edit]
React Developer Skill
Professional React development skill based on Arcana React enterprise architecture.
⚡ Workflow — Always Start From the Reference Project
Every task starts by cloning the complete reference project — NEVER scaffold from scratch (no create-react-app / vite create):
git clone https://github.com/jrjohn/arcana-react.git [new-project-directory]
- Clone the reference project (command above).
- Build + test the UNTOUCHED clone first —
npm install && npm run build && npm run testmust be green before any modification (known-good baseline). - Follow 0. Project Setup to rename the project and strip demo code — KEEP the infrastructure: auth (guards/interceptors), 4-layer caching, offline/sync, security layers, DI/core (Context providers), router skeleton.
- Add features following the File-by-File Feature Recipe below.
Supporting files — load on demand
| File | When to read |
|---|---|
patterns.md |
Detailed design patterns beyond the core examples in this file |
patterns/mvvm-input-output.md |
Deep-dive on the MVVM Input/Output/Effect ViewModel pattern |
examples.md |
Full working code examples for complete features |
checklists/production-ready.md |
Pre-release production & code review checklists |
verification/commands.md |
Complete catalog of verification bash commands |
Quick Reference Card
New Screen Checklist:
1. Add route → router/routes.tsx (path + element)
2. Create Component with React.memo for optimization
3. Create useViewModel hook (Input/Output/Effect pattern)
4. Create template with Loading/Error/Empty states
5. Wire navigation callbacks in parent component
6. Verify mock data returns non-empty values
New Repository Checklist:
1. Interface → domain/repositories/xxx.repository.ts
2. Implementation → data/repositories/xxx.repository.impl.ts
3. Mock → data/repositories/mock/mock-xxx.repository.ts
4. Provider binding → core/providers/RepositoryProvider.tsx
5. Mock data (NEVER return [] or null!)
6. Verify ID consistency across repositories
Quick Diagnosis:
| Symptom | Check Command |
|---|---|
| Blank screen | `grep -rn "[]\ |
| Navigation crash | Compare routes.tsx paths vs component imports |
| Button does nothing | `grep -rn "onClick={undefined}\ |
| Data not loading | `grep -rn "throw.*NotImplemented\ |
File-by-File Feature Recipe
Create files in this order when adding a new feature (example: project):
1. Model -> domain/models/project.model.ts
2. Validator -> domain/validators/project.validator.ts
3. Service Interface -> domain/services/project.service.ts
4. Service Impl -> domain/services/project.service.impl.ts
5. Repository Interface -> domain/repositories/project.repository.ts
6. Repository Impl -> data/repositories/project.repository.impl.ts
7. DTO -> data/remote/dtos/project.dto.ts
8. Mapper -> data/remote/mappers/project.mapper.ts
9. Mock Repository -> data/repositories/mock/mock-project.repository.ts
10. Provider Binding -> core/providers/RepositoryProvider.tsx (+ useProjectRepository hook)
11. ViewModel Hook -> presentation/pages/projects/useProjectViewModel.ts
12. Component -> presentation/pages/projects/ProjectPage.tsx
13. NavGraph Methods -> core/services/nav-graph.service.ts (toProjectList, toProjectDetail)
14. Route -> router/routes.tsx (path + element)
15. Tests -> useProjectViewModel.test.ts + service/repository tests
Step notes:
- 1-2: Model is a plain TypeScript interface; validator is a pure function module (no React imports).
- 3-6: Service depends on the repository INTERFACE (step 5), never the impl. Repository impl wraps the 4-layer cache + offline sync.
- 7-8: DTO mirrors the API payload exactly; mapper converts DTO <-> domain model. Never leak DTOs above the data layer.
- 9: Mock data must be realistic and non-empty (see Mock Data Rules — NEVER return
[]). - 10: Register both impl and mock in
RepositoryProvider.tsx(mock for DEV) and export auseProjectRepository()hook. - 11: Hook follows the Input/Output/Effect pattern (discriminated-union
Input, memoizedOutput, RxJSSubjectfor effects). - 12: Wrap with
React.memo, render Loading/Error/Empty/Content states, subscribe toeffect$inuseEffect. - 13-14: Every route added in
routes.tsxMUST get a matching NavGraph method, and every navigation callback prop must be bound in the parent. - 15: ViewModel hook tests first (90%+ target), then service (85%+) and repository (80%+) tests.
Verify after wiring: npm run type-check && npm run build && npm run test.
Rules Priority
CRITICAL (Must Fix Immediately)
| Rule | Description | Verification |
|---|---|---|
| Zero-Empty Policy | Repository stubs NEVER return empty arrays | grep "\[\]" *.impl.ts |
| Route Wiring | ALL routes MUST have component imports | Count paths vs components |
| Callback Binding | ALL navigation callbacks MUST be bound | Check props drilling |
| ID Consistency | Cross-repository IDs must match | Check mock data IDs |
IMPORTANT (Should Fix Before PR)
| Rule | Description | Verification |
|---|---|---|
| UI States | Loading/Error/Empty for all screens | grep -L "isLoading" *.viewmodel.ts |
| Mock Data Quality | Realistic, varied values (not all same) | Review mock data arrays |
| Error Messages | User-friendly, not technical errors | Check error handling |
| React.memo | Performance-critical components memoized | Check component exports |
RECOMMENDED (Nice to Have)
| Rule | Description |
|---|---|
| Animations | Smooth route transitions with Framer Motion |
| Accessibility | ARIA labels for interactive elements |
| Dark Mode | Support system theme preference |
| PWA | Service worker for offline |
Error Handling Pattern
AppError - Unified Error Model
// domain/models/app-error.ts
export type AppError =
| { type: 'NETWORK_UNAVAILABLE' }
| { type: 'TIMEOUT' }
| { type: 'SERVER_ERROR'; statusCode: number }
| { type: 'UNAUTHORIZED' }
| { type: 'TOKEN_EXPIRED' }
| { type: 'INVALID_CREDENTIALS' }
| { type: 'NOT_FOUND' }
| { type: 'VALIDATION_FAILED'; message: string }
| { type: 'DATA_CORRUPTED' }
| { type: 'UNKNOWN'; underlying: Error };
export function getErrorMessage(error: AppError): string {
switch (error.type) {
case 'NETWORK_UNAVAILABLE':
return 'No internet connection. Please check your network.';
case 'TIMEOUT':
return 'Request timed out. Please try again.';
case 'SERVER_ERROR':
return `Server error (${error.statusCode}). Please try again later.`;
case 'UNAUTHORIZED':
case 'TOKEN_EXPIRED':
return 'Session expired. Please login again.';
case 'INVALID_CREDENTIALS':
return 'Invalid email or password.';
case 'NOT_FOUND':
return 'The requested item was not found.';
case 'VALIDATION_FAILED':
return error.message;
case 'DATA_CORRUPTED':
return 'Data error. Please contact support.';
case 'UNKNOWN':
return 'An unexpected error occurred.';
}
}
export function requiresReauth(error: AppError): boolean {
return error.type === 'UNAUTHORIZED' || error.type === 'TOKEN_EXPIRED';
}
Error Handling Flow
┌─────────────────────────────────────────────────────────────────┐
│ Error Flow │
├─────────────────────────────────────────────────────────────────┤
│ Repository Layer: │
│ - Catch HTTP errors (Axios interceptors) │
│ - Map to AppError │
│ - Throw AppError │
├─────────────────────────────────────────────────────────────────┤
│ Service Layer: │
│ - Catch repository errors │
│ - Add business context if needed │
│ - Re-throw as AppError │
├─────────────────────────────────────────────────────────────────┤
│ ViewModel Hook: │
│ - Catch all errors │
│ - Update error state with getErrorMessage() │
│ - Check requiresReauth() for auth redirect │
├─────────────────────────────────────────────────────────────────┤
│ Component Layer: │
│ - Display error from output.error │
│ - Show retry button │
│ - Handle auth redirect via useEffect │
└─────────────────────────────────────────────────────────────────┘
Error Handling by Layer
Axios Interceptor:
// core/api/axios-interceptor.ts
import axios, { AxiosError } from 'axios';
import { AppError } from '../../domain/models/app-error';
export const apiClient = axios.create({
baseURL: import.meta.env.VITE_API_URL,
timeout: 30000,
});
apiClient.interceptors.response.use(
(response) => response,
(error: AxiosError) => {
let appError: AppError;
if (!error.response) {
appError = { type: 'NETWORK_UNAVAILABLE' };
} else if (error.response.status === 401) {
appError = { type: 'UNAUTHORIZED' };
} else if (error.response.status === 404) {
appError = { type: 'NOT_FOUND' };
} else if (error.response.status >= 500) {
appError = { type: 'SERVER_ERROR', statusCode: error.response.status };
} else {
appError = { type: 'UNKNOWN', underlying: error };
}
return Promise.reject(appError);
}
);
ViewModel Hook:
const loadData = useCallback(async () => {
setIsLoading(true);
setError(null);
try {
const items = await repository.getItems();
setItems(items);
} catch (error) {
const appError = error as AppError;
setError(getErrorMessage(appError));
if (requiresReauth(appError)) {
emitEffect({ type: 'NAVIGATE_TO_LOGIN' });
}
} finally {
setIsLoading(false);
}
}, [repository]);
Test Coverage Targets
Coverage by Layer
| Layer | Target | Focus Areas |
|---|---|---|
| ViewModel Hooks | 90%+ | All Input handlers, state transitions, effects |
| Service | 85%+ | Business logic, edge cases |
| Repository | 80%+ | Data mapping, error handling |
| Component | 60%+ | Template binding, user interactions |
What to Test
ViewModel Hook Tests (Highest Priority):
describe('useFeatureViewModel', () => {
// Test each Input type
it('should load items on LOAD input', async () => { });
it('should set error on LOAD failure', async () => { });
it('should emit toast on REFRESH success', async () => { });
it('should navigate on ITEM_CLICKED', () => { });
// Test state transitions
it('should set isLoading true then false', async () => { });
// Test edge cases
it('should show empty state when no items', async () => { });
});
Service Tests:
describe('UserService', () => {
// Test business rules
it('should validate email format', () => { });
it('should calculate score correctly', () => { });
});
Repository Tests:
describe('UserRepository', () => {
// Test data mapping
it('should map DTO to domain model', async () => { });
// Test offline behavior
it('should return cached data when offline', async () => { });
});
Test Commands
# Run all tests with coverage
npm run test -- --coverage
# Run tests in watch mode
npm run test
# View coverage report
open coverage/index.html
Core Architecture Principles
Clean Architecture - Three Layers
┌─────────────────────────────────────────────────────┐
│ Presentation Layer │
│ Components + Custom Hooks + Input/Output/Effect │
├─────────────────────────────────────────────────────┤
│ Domain Layer │
│ Business Logic + Services + Models │
├─────────────────────────────────────────────────────┤
│ Data Layer │
│ Offline-First Repository + IndexedDB + 4L Cache │
└─────────────────────────────────────────────────────┘
Dependency Rules
- Unidirectional Dependencies: Presentation → Domain → Data
- Interface Segregation: Decouple layers through interfaces
- Dependency Inversion: Data layer implements Domain layer interfaces
Instructions
When handling React development tasks, follow these principles:
Quick Verification Commands
Use these commands to quickly check for common issues:
# 1. Check for unimplemented services (MUST be empty)
grep -rn "throw.*NotImplemented\|TODO.*implement" src/
# 2. Check for empty click handlers (MUST be empty)
grep -rn "onClick={undefined}\|onClick={() => {}}" src/
# 3. Check for missing route components (compare routes vs components)
echo "Routes defined:" && grep -c "path:" src/router/routes.tsx 2>/dev/null || echo 0
echo "Components imported:" && grep -c "element:" src/router/routes.tsx 2>/dev/null || echo 0
# 4. Check NavGraph has all navigation methods
grep -c "to\|navigate" src/core/services/nav-graph.service.ts 2>/dev/null || echo 0
# 5. Verify build compiles
npm run build
# 6. Check for navigation callbacks not wired (CRITICAL!)
echo "=== Navigation Props Defined ===" && \
grep -rh "onNavigate" src/presentation/ | grep -oE "onNavigate[A-Za-z]*" | sort -u
echo "=== Navigation Props Used ===" && \
grep -rh "onNavigate.*=" src/presentation/**/*.tsx 2>/dev/null | grep -oE "onNavigate[A-Za-z]*" | sort -u
# 7. Verify ALL routes have NavGraph navigation methods
echo "=== Routes Defined ===" && \
grep -rh "path:" src/router/routes.tsx | grep -oE "'[^']+'" | sort -u
echo "=== NavGraph Methods ===" && \
grep -rh "to[A-Z][a-zA-Z]*\(" src/core/services/nav-graph.service.ts | grep -oE "to[A-Z][a-zA-Z]*" | sort -u
# 8. Check Service→Repository wiring (CRITICAL!)
echo "=== Repository Methods Called in Services ===" && \
grep -roh "this\.[a-zA-Z]*Repository\.[a-zA-Z]*(" src/domain/services/*.ts | sort -u
echo "=== Repository Interface Methods ===" && \
grep -rh "[a-zA-Z]*\(" src/domain/repositories/*.repository.ts | grep -oE "[a-zA-Z]+\(" | sort -u
# 9. Check for empty array returns in Repository stubs (MUST FIX)
grep -rn "\[\]" src/data/repositories/*.repository.impl.ts
# 10. TypeScript type checking
npm run type-check
CRITICAL: All routes in routes.tsx MUST have corresponding component imports. Missing components cause runtime errors.
Mock Data Requirements for Repository Stubs
The Chart Data Problem
When implementing Repository stubs, NEVER return empty arrays for data that powers UI charts or visualizations. This causes:
- Charts that render but show nothing (blank Recharts/Chart.js canvas)
- Line charts that skip rendering (e.g.,
if (data.length < 2) return;) - Empty state components even when data structure exists
Mock Data Rules
Rule 1: Array data for charts MUST have at least 7 items
// BAD - Chart will be blank
getCurrentWeekSummary(): Promise<WeeklySummary> {
return Promise.resolve({
dailyReports: [] // ← Chart has no data to render!
});
}
// GOOD - Chart has data to display
getCurrentWeekSummary(): Promise<WeeklySummary> {
const mockDailyReports = Array.from({ length: 7 }, (_, i) =>
this.createMockDailyReport(
[72, 78, 85, 80, 76, 88, 82][i],
[390, 420, 450, 410, 380, 460, 435][i]
)
);
return Promise.resolve({ dailyReports: mockDailyReports });
}
Rule 2: Use realistic, varied sample values
// BAD - Monotonous test data
const scores = Array(7).fill(80);
// GOOD - Realistic variation
const scores = [72, 78, 85, 80, 76, 88, 82]; // Shows trend
Rule 3: Data must match interface exactly
# Before creating mock data, ALWAYS verify the interface definition:
grep -A 20 "interface TherapyData" src/domain/models/*.ts
Rule 4: Create helper functions for complex mock data
// Create reusable mock factory
private createMockDailyReport(score: number, duration: number): DailySleepReport {
return {
id: `mock_${Date.now()}`,
sleepScore: score,
sleepDuration: { totalMinutes: duration, ... },
// ... all required fields
};
}
0. Project Setup - CRITICAL
IMPORTANT: This reference project has been validated with tested npm/React settings and library versions. NEVER reconfigure project structure or modify package.json / vite.config.ts, or it will cause compilation errors.
Step 1: Clone the reference project
git clone https://github.com/jrjohn/arcana-react.git [new-project-directory]
cd [new-project-directory]
Step 2: Reinitialize Git (remove original repo history)
rm -rf .git
git init
git add .
git commit -m "Initial commit from arcana-react template"
Step 3: Modify project name Only modify the following required items:
namefield inpackage.json<title>inindex.html- Update related settings in environment configuration files
Step 4: Clean up example code The cloned project contains example UI. Clean up and replace with new project screens:
Core architecture files to KEEP (do not delete):
src/core/- Common utilities (Guards, Interceptors, Services)src/shared/- Shared components and hookssrc/data/local/- IndexedDB (Dexie) base configurationsrc/data/repositories/- Repository base classessrc/App.tsx- App entry pointsrc/router/routes.tsx- Route configuration (modify routes)
Example files to REPLACE:
src/presentation/- Delete all example screens, create new project Componentssrc/domain/models/- Delete example Models, create new Domain Modelssrc/data/remote/- Modify API endpointssrc/assets/- Update resource files
Step 5: Install dependencies and verify build
npm install
npm run build
Prohibited Actions
- DO NOT create new React project from scratch (create-react-app/vite create)
- DO NOT modify version numbers in
package.json - DO NOT add or remove npm dependencies (unless explicitly required)
- DO NOT modify build settings in
vite.config.ts - DO NOT reconfigure Tailwind, React Router, or other library settings
Allowed Modifications
- Add business-related TypeScript code (following existing architecture)
- Add Components, Services, Custom Hooks
- Add Domain Models, Repository
- Modify resources in
src/assets/ - Add route configurations
1. TDD & Spec-Driven Development Workflow - MANDATORY
CRITICAL: All development MUST follow this TDD workflow. Every Spec requirement must have corresponding tests BEFORE implementation.
ABSOLUTE RULE: TDD = Tests + Implementation. Writing tests without implementation is INCOMPLETE. Every test file MUST have corresponding production code that passes the tests.
┌─────────────────────────────────────────────────────────────────┐
│ TDD Development Workflow │
├─────────────────────────────────────────────────────────────────┤
│ Step 1: Analyze Spec → Extract all SRS & SDD requirements │
│ Step 2: Create Tests → Write tests for EACH Spec item │
│ Step 3: Verify Coverage → Ensure 100% Spec coverage in tests │
│ Step 4: Implement → Build features to pass tests ⚠️ MANDATORY │
│ Step 5: Mock APIs → Use mock data for unfinished Cloud APIs │
│ Step 6: Run All Tests → ALL tests must pass before completion │
│ Step 7: Verify 100% → Tests written = Features implemented │
└─────────────────────────────────────────────────────────────────┘
Placeholder Component Policy
Placeholder components are ONLY allowed as a temporary route during active development. They are FORBIDDEN as a final state.
// WRONG - Placeholder component left in production
{ path: 'training', element: <PlaceholderComponent /> } // FORBIDDEN!
// CORRECT - Real component implementation
{ path: 'training', element: <TrainingPage /> }
Placeholder Check Command:
# This command MUST return empty for production-ready code
grep -rn "PlaceholderComponent\|throw.*NotImplemented\|TODO.*implement\|Coming Soon" src/
2. Project Structure
src/
├── presentation/ # UI Layer
│ ├── components/ # Smart & Dumb Components
│ ├── layouts/ # Page Layouts
│ └── pages/ # Page Components
├── domain/ # Domain Layer
│ ├── models/ # Domain Models
│ ├── services/ # Business Services
│ └── repositories/ # Repository Interfaces
├── data/ # Data Layer
│ ├── repositories/ # Repository Implementations
│ ├── local/ # IndexedDB (Dexie)
│ └── remote/ # API Client (Axios)
├── core/ # Core utilities
│ ├── providers/ # Context Providers
│ ├── hooks/ # Shared hooks
│ └── services/ # Core services
├── shared/ # Shared components
├── router/ # React Router config
└── assets/
3. ViewModel Input/Output/Effect Pattern with Hooks
import { useState, useCallback, useMemo, useRef, useEffect } from 'react';
import { Subject } from 'rxjs';
// Input: Discriminated union defining all events
export type UserInput =
| { type: 'UPDATE_NAME'; name: string }
| { type: 'UPDATE_EMAIL'; email: string }
| { type: 'SUBMIT' };
// Output: State container
export interface UserOutput {
name: string;
email: string;
isLoading: boolean;
error: string | null;
}
// Effect: One-time events
export type UserEffect =
| { type: 'NAVIGATE_BACK' }
| { type: 'SHOW_TOAST'; message: string };
export function useUserViewModel(userService: UserService) {
// State
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
// Effect stream (using RxJS Subject)
const effectRef = useRef(new Subject<UserEffect>());
const effect$ = effectRef.current.asObservable();
// Output (memoized)
const output = useMemo<UserOutput>(() => ({
name,
email,
isLoading,
error,
}), [name, email, isLoading, error]);
// Input handler
const onInput = useCallback(async (input: UserInput) => {
switch (input.type) {
case 'UPDATE_NAME':
setName(input.name);
break;
case 'UPDATE_EMAIL':
setEmail(input.email);
break;
case 'SUBMIT':
await handleSubmit();
break;
}
}, [name, email]);
const handleSubmit = useCallback(async () => {
setIsLoading(true);
setError(null);
try {
await userService.updateUser({ name, email });
effectRef.current.next({ type: 'NAVIGATE_BACK' });
} catch (err) {
setError(err instanceof Error ? err.message : 'Unknown error');
} finally {
setIsLoading(false);
}
}, [name, email, userService]);
return { output, effect$, onInput };
}
4. Four-Layer Offline-First Caching
import Dexie from 'dexie';
interface CacheEntry<T> {
value: T;
timestamp: number;
}
export class CacheManager<T> {
// L1: Memory cache (<1ms)
private memoryCache = new Map<string, CacheEntry<T>>();
// L2: LRU + TTL cache (~2ms)
private lruCache = new Map<string, CacheEntry<T>>();
private readonly maxLruSize = 100;
private readonly ttlMs = 5 * 60 * 1000; // 5 minutes
// L3: IndexedDB persistence (~10ms)
private db: Dexie;
constructor(private readonly tableName: string) {
this.db = new Dexie('AppCache');
this.db.version(1).stores({
cache: 'key, value, timestamp',
});
}
async get(key: string, loader: () => Promise<T>): Promise<T> {
const now = Date.now();
// Check L1: Memory cache
const memEntry = this.memoryCache.get(key);
if (memEntry && now - memEntry.timestamp < this.ttlMs) {
return memEntry.value;
}
// Check L2: LRU cache
const lruEntry = this.lruCache.get(key);
if (lruEntry && now - lruEntry.timestamp < this.ttlMs) {
this.memoryCache.set(key, lruEntry);
return lruEntry.value;
}
// Check L3: IndexedDB
const dbEntry = await this.db.table('cache').get(key);
if (dbEntry && now - dbEntry.timestamp < this.ttlMs) {
const entry = { value: dbEntry.value, timestamp: dbEntry.timestamp };
this.memoryCache.set(key, entry);
this.addToLru(key, entry);
return dbEntry.value;
}
// L4: Load from remote
const value = await loader();
const entry = { value, timestamp: now };
this.memoryCache.set(key, entry);
this.addToLru(key, entry);
await this.db.table('cache').put({ key, ...entry });
return value;
}
private addToLru(key: string, entry: CacheEntry<T>): void {
if (this.lruCache.size >= this.maxLruSize) {
const oldestKey = this.lruCache.keys().next().value;
if (oldestKey) this.lruCache.delete(oldestKey);
}
this.lruCache.set(key, entry);
}
}
5. Component with React.memo
import React, { memo, useEffect } from 'react';
import { useUserViewModel, UserInput } from './useUserViewModel';
import { useNavGraph } from '../../core/hooks/useNavGraph';
import { useToast } from '../../shared/hooks/useToast';
interface UserFormProps {
userService: UserService;
}
export const UserForm = memo<UserFormProps>(({ userService }) => {
const { output, effect$, onInput } = useUserViewModel(userService);
const navGraph = useNavGraph();
const toast = useToast();
// Handle effects
useEffect(() => {
const subscription = effect$.subscribe((effect) => {
switch (effect.type) {
case 'NAVIGATE_BACK':
navGraph.back();
break;
case 'SHOW_TOAST':
toast.show(effect.message);
break;
}
});
return () => subscription.unsubscribe();
}, [effect$, navGraph, toast]);
const handleNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
onInput({ type: 'UPDATE_NAME', name: e.target.value });
};
const handleEmailChange = (e: React.ChangeEvent<HTMLInputElement>) => {
onInput({ type: 'UPDATE_EMAIL', email: e.target.value });
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
onInput({ type: 'SUBMIT' });
};
return (
<form onSubmit={handleSubmit}>
<div className="form-group">
<label htmlFor="name">Name</label>
<input
id="name"
type="text"
value={output.name}
onChange={handleNameChange}
/>
</div>
<div className="form-group">
<label htmlFor="email">Email</label>
<input
id="email"
type="email"
value={output.email}
onChange={handleEmailChange}
/>
</div>
{output.error && (
<div className="error">{output.error}</div>
)}
<button type="submit" disabled={output.isLoading}>
{output.isLoading ? 'Loading...' : 'Submit'}
</button>
</form>
);
});
UserForm.displayName = 'UserForm';
6. Type-Safe Navigation (NavGraph Service)
// core/services/nav-graph.service.ts
import { useNavigate } from 'react-router-dom';
import { useCallback, useMemo } from 'react';
export function useNavGraph() {
const navigate = useNavigate();
return useMemo(() => ({
toHome: () => navigate('/'),
toUserList: () => navigate('/users'),
toUserDetail: (userId: string) => navigate(`/users/${userId}`),
toUserEdit: (userId: string) => navigate(`/users/${userId}/edit`),
toProjectList: () => navigate('/projects'),
toProjectDetail: (projectId: string) => navigate(`/projects/${projectId}`),
toLogin: () => navigate('/login'),
toForgotPassword: () => navigate('/forgot-password'),
back: () => navigate(-1),
}), [navigate]);
}
7. Context-Based Dependency Injection
// core/providers/RepositoryProvider.tsx
import React, { createContext, useContext, useMemo } from 'react';
interface Repositories {
userRepository: IUserRepository;
projectRepository: IProjectRepository;
authRepository: IAuthRepository;
}
const RepositoryContext = createContext<Repositories | null>(null);
interface RepositoryProviderProps {
children: React.ReactNode;
useMock?: boolean;
}
export function RepositoryProvider({ children, useMock = false }: RepositoryProviderProps) {
const repositories = useMemo<Repositories>(() => {
if (useMock || import.meta.env.DEV) {
return {
userRepository: new MockUserRepository(),
projectRepository: new MockProjectRepository(),
authRepository: new MockAuthRepository(),
};
}
return {
userRepository: new UserRepositoryImpl(),
projectRepository: new ProjectRepositoryImpl(),
authRepository: new AuthRepositoryImpl(),
};
}, [useMock]);
return (
<RepositoryContext.Provider value={repositories}>
{children}
</RepositoryContext.Provider>
);
}
export function useRepositories(): Repositories {
const context = useContext(RepositoryContext);
if (!context) {
throw new Error('useRepositories must be used within RepositoryProvider');
}
return context;
}
// Individual repository hooks
export function useUserRepository(): IUserRepository {
return useRepositories().userRepository;
}
export function useProjectRepository(): IProjectRepository {
return useRepositories().projectRepository;
}
export function useAuthRepository(): IAuthRepository {
return useRepositories().authRepository;
}
8. HTTP Interceptors with Axios
// core/api/axios-config.ts
import axios, { AxiosInstance, InternalAxiosRequestConfig, AxiosResponse, AxiosError } from 'axios';
export function createApiClient(getToken: () => string | null): AxiosInstance {
const client = axios.create({
baseURL: import.meta.env.VITE_API_URL,
timeout: 30000,
});
// Request interceptor - Add auth token
client.interceptors.request.use(
(config: InternalAxiosRequestConfig) => {
const token = getToken();
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => Promise.reject(error)
);
// Response interceptor - Handle errors
client.interceptors.response.use(
(response: AxiosResponse) => response,
(error: AxiosError) => {
if (error.response?.status === 401) {
// Handle token expiry
window.dispatchEvent(new CustomEvent('auth:logout'));
}
return Promise.reject(error);
}
);
return client;
}
Navigation Wiring Verification Guide
The Navigation Wiring Blind Spot
React Components often have navigation callback props that need parent binding:
// SettingsPanel.tsx
interface SettingsPanelProps {
onNavigateToAccountInfo?: () => void; // Needs parent binding!
onNavigateToChangePassword?: () => void; // Needs parent binding!
onNavigateToUserList?: () => void; // Needs parent binding!
}
export const SettingsPanel: React.FC<SettingsPanelProps> = ({
onNavigateToAccountInfo,
onNavigateToChangePassword,
onNavigateToUserList,
}) => {
// If props are not provided, buttons do nothing!
return (
<button onClick={onNavigateToAccountInfo}>Account Info</button>
);
};
Problem: If the parent Component doesn't pass these props, the buttons appear functional but do nothing when clicked!
Correct Wiring Example
// SettingsPage.tsx (Parent - correctly wired)
import { useNavGraph } from '../../core/hooks/useNavGraph';
import { SettingsPanel } from './SettingsPanel';
export const SettingsPage: React.FC = () => {
const navGraph = useNavGraph();
return (
<SettingsPanel
onNavigateToAccountInfo={navGraph.toAccountInfo}
onNavigateToChangePassword={navGraph.toChangePassword}
onNavigateToUserList={navGraph.toUserList}
/>
);
};
// routes.tsx (routes exist)
export const routes: RouteObject[] = [
{ path: '/account-info', element: <AccountInfoPage /> }, // Route exists
{ path: '/change-password', element: <ChangePasswordPage /> }, // Route exists
{ path: '/user-list', element: <UserListPage /> }, // Route exists
];
Code Review Checklist
Required Items
- Follow Clean Architecture layering
- ViewModel uses Input/Output/Effect pattern with hooks
- Repository implements offline-first with IndexedDB
- Components use React.memo for optimization
- Type-safe navigation via useNavGraph hook
- No implicit
anytypes (strict mode) - ALL navigation callbacks are bound in parent components
- ALL routes have corresponding NavGraph methods
- ALL Service→Repository method calls exist in Repository interfaces
- ALL Repository interface methods have implementations
Performance Checks
- Use React.memo across presentational components
- Use useMemo/useCallback for expensive computations
- Implement virtual scrolling for large datasets (react-window)
- Use lazy loading and code splitting (React.lazy)
Security Checks
- Content Security Policy headers configured
- Input sanitization (DOMPurify for HTML)
- Axios interceptors for auth and error handling
- XSS/CSRF protection enabled
- No hardcoded API keys
Common Issues
Hook Dependency Issues
- Include all dependencies in useEffect/useCallback/useMemo
- Use ESLint react-hooks/exhaustive-deps rule
- Avoid stale closures by proper dependency management
IndexedDB Issues
- Handle version upgrades properly with Dexie
- Use transactions for batch operations
- Implement proper error handling
Build Optimization
- Enable production mode
- Configure tree shaking in Vite
- Use lazy loading for route components
Spec Gap Prediction System
When Spec is incomplete, use these universal rules to predict and supplement missing UI/UX elements.
Screen Type → Required States (Universal)
| Screen Type | Required States | Auto-Predict |
|---|---|---|
| List Screen | Loading, Error, Empty, Content | Virtual scroll, Search/Filter |
| Detail Screen | Loading, Error, Content | Back navigation, Edit action |
| Form Screen | Validation, Submit Loading, Success, Error | Input validation, Cancel action |
| Dashboard | Loading, Error, Content | Refresh, Section navigation |
| Settings | Content | Back navigation, Section headers |
| Auth Screen | Loading, Error, Success | Forgot password link, Terms link |
Prediction Implementation Example
When implementing a List screen from Spec:
// Spec says: "Display user's items"
// Auto-predict required implementation:
export const ItemList: React.FC = () => {
const { output, onInput } = useItemListViewModel();
// 1. LOADING - Always needed for API/DB calls
if (output.isLoading) {
return (
<div className="flex justify-center items-center h-64">
<Spinner />
</div>
);
}
// 2. ERROR - Always needed for API/DB calls
if (output.error) {
return (
<div className="text-center p-8">
<p className="text-red-500">{output.error}</p>
<button
className="btn-primary mt-4"
onClick={() => onInput({ type: 'RETRY' })}
>
Retry
</button>
</div>
);
}
// 3. EMPTY - Always needed for list screens
if (output.items.length === 0) {
return (
<div className="text-center p-8">
<h3 className="text-xl font-semibold">No Items</h3>
<p className="text-gray-500">Add your first item to get started</p>
<button
className="btn-primary mt-4"
onClick={() => onInput({ type: 'ADD_CLICKED' })}
>
Add Item
</button>
</div>
);
}
// 4. CONTENT - The actual list
return (
<ul className="divide-y">
{output.items.map((item) => (
<li
key={item.id}
className="p-4 hover:bg-gray-50 cursor-pointer"
onClick={() => onInput({ type: 'ITEM_CLICKED', id: item.id })}
>
{item.name}
</li>
))}
</ul>
);
};
Tech Stack Reference
| Technology | Recommended Version |
|---|---|
| React | 19.1+ |
| TypeScript | 5.8+ |
| Vite | 6.3+ |
| React Router | 7.0+ |
| Axios | Latest |
| RxJS | 7.8+ |
| Tailwind CSS | 4.0+ |
| Dexie | 4.0+ |
| react-i18next | Latest |
| Vitest | Latest |
| React Testing Library | Latest |