| 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 test must 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 by copying the nearest working feature, then adapting (see 🔁 Adding a feature) — the File-by-File Feature Recipe below is the completeness checklist.
🔁 Adding a feature = copy the nearest working feature (NOT from scratch)
When you add a new feature, do NOT re-create it from memory by walking the File-by-File Recipe on a blank slate. Copy the nearest already-working feature in the cloned reference, then adapt it.
- Find the closest conformant feature already in the reference — e.g. the example
users feature (user-list / user-detail / user-form) — it already has the full Model → Repository → Service → ViewModel (Input/Output/Effect) → Component → specs chain wired through DI.
- Duplicate its ENTIRE file set (model, validator, service interface + impl, repository interface + impl, DTO, mapper, mock repository, provider binding +
useXxxRepository hook, ViewModel hook, component, NavGraph methods, route, tests) 1:1, keeping every layer.
- Rename + adapt to the new domain (types, routes, endpoints, DI bindings/providers).
- Diff against the original to confirm nothing was dropped — same layer split, same ViewModel/service/repository boundary, same error model, same tests.
Why this is mandatory: the File-by-File Recipe is a checklist of what must exist, not a from-scratch build order. Re-deriving the pattern each time makes every step a chance to skip a layer (the ViewModel, the repository), wire a shortcut (component → data/API directly), or drop the tests — the "vibe-coding" deviations that pass local build + coverage but fail architecture review (e.g. arch-qube). Copying a known-good feature carries conformance in by construction; the recipe is then only your verification that nothing is missing.
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 a useProjectRepository() hook.
- 11: Hook follows the Input/Output/Effect pattern (discriminated-union
Input, memoized Output, RxJS Subject for effects).
- 12: Wrap with
React.memo, render Loading/Error/Empty/Content states, subscribe to effect$ in useEffect.
- 13-14: Every route added in
routes.tsx MUST 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
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:
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', () => {
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', () => { });
it('should set isLoading true then false', async () => { });
it('should show empty state when no items', async () => { });
});
Service Tests:
describe('UserService', () => {
it('should validate email format', () => { });
it('should calculate score correctly', () => { });
});
Repository Tests:
describe('UserRepository', () => {
it('should map DTO to domain model', async () => { });
it('should return cached data when offline', async () => { });
});
Test Commands
npm run test -- --coverage
npm run test
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:
grep -rn "throw.*NotImplemented\|TODO.*implement" src/
grep -rn "onClick={undefined}\|onClick={() => {}}" src/
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
grep -c "to\|navigate" src/core/services/nav-graph.service.ts 2>/dev/null || echo 0
npm run build
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
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
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
grep -rn "\[\]" src/data/repositories/*.repository.impl.ts
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
getCurrentWeekSummary(): Promise<WeeklySummary> {
return Promise.resolve({
dailyReports: []
});
}
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
const scores = Array(7).fill(80);
const scores = [72, 78, 85, 80, 76, 88, 82];
Rule 3: Data must match interface exactly
grep -A 20 "interface TherapyData" src/domain/models/*.ts
Rule 4: Create helper functions for complex mock data
private createMockDailyReport(score: number, duration: number): DailySleepReport {
return {
id: `mock_${Date.now()}`,
sleepScore: score,
sleepDuration: { totalMinutes: duration, ... },
};
}
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:
name field in package.json
<title> in index.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 hooks
src/data/local/ - IndexedDB (Dexie) base configuration
src/data/repositories/ - Repository base classes
src/App.tsx - App entry point
src/router/routes.tsx - Route configuration (modify routes)
Example files to REPLACE:
src/presentation/ - Delete all example screens, create new project Components
src/domain/models/ - Delete example Models, create new Domain Models
src/data/remote/ - Modify API endpoints
src/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.
{ path: 'training', element: <PlaceholderComponent /> }
{ path: 'training', element: <TrainingPage /> }
Placeholder Check Command:
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';
export type UserInput =
| { type: 'UPDATE_NAME'; name: string }
| { type: 'UPDATE_EMAIL'; email: string }
| { type: 'SUBMIT' };
export interface UserOutput {
name: string;
email: string;
isLoading: boolean;
error: string | null;
}
export type UserEffect =
| { type: 'NAVIGATE_BACK' }
| { type: 'SHOW_TOAST'; message: string };
export function useUserViewModel(userService: UserService) {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const effectRef = useRef(new Subject<UserEffect>());
const effect$ = effectRef.current.asObservable();
const output = useMemo<UserOutput>(() => ({
name,
email,
isLoading,
error,
}), [name, email, isLoading, error]);
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> {
private memoryCache = new Map<string, CacheEntry<T>>();
private lruCache = new Map<string, CacheEntry<T>>();
private readonly maxLruSize = 100;
private readonly ttlMs = 5 * 60 * 1000;
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();
const memEntry = this.memoryCache.get(key);
if (memEntry && now - memEntry.timestamp < this.ttlMs) {
return memEntry.value;
}
const lruEntry = this.lruCache.get(key);
if (lruEntry && now - lruEntry.timestamp < this.ttlMs) {
this.memoryCache.set(key, lruEntry);
return lruEntry.value;
}
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;
}
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();
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)
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
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;
}
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
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,
});
client.interceptors.request.use(
(config: InternalAxiosRequestConfig) => {
const token = getToken();
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => Promise.reject(error)
);
client.interceptors.response.use(
(response: AxiosResponse) => response,
(error: AxiosError) => {
if (error.response?.status === 401) {
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:
interface SettingsPanelProps {
onNavigateToAccountInfo?: () => void;
onNavigateToChangePassword?: () => void;
onNavigateToUserList?: () => void;
}
export const SettingsPanel: React.FC<SettingsPanelProps> = ({
onNavigateToAccountInfo,
onNavigateToChangePassword,
onNavigateToUserList,
}) => {
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
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}
/>
);
};
export const routes: RouteObject[] = [
{ path: '/account-info', element: <AccountInfoPage /> },
{ path: '/change-password', element: <ChangePasswordPage /> },
{ path: '/user-list', element: <UserListPage /> },
];
Code Review Checklist
Required Items
Performance Checks
Security Checks
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:
export const ItemList: React.FC = () => {
const { output, onInput } = useItemListViewModel();
if (output.isLoading) {
return (
<div className="flex justify-center items-center h-64">
<Spinner />
</div>
);
}
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>
);
}
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>
);
}
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 |