Document common pitfalls for the frontend and enhance architecture guidelines

- Added `COMMON_PITFALLS.md` to document frequent mistakes and best practices in frontend development, focusing on React Hooks, Context API, Zustand patterns, TypeScript type safety, and more.
- Updated `ARCHITECTURE.md` with detailed insights on the `AuthContext` dependency injection pattern, including usage examples, provider tree structure, polymorphic hooks, and testing strategies.
- Emphasized compliance with React Rules of Hooks, performance optimizations, and separation of concerns in component design.
- Included implementation-ready examples, checklists, and resources to guide maintainable and testable frontend development.
This commit is contained in:
Felipe Cardoso
2025-11-03 11:59:21 +01:00
parent a36c1b61bb
commit 2ee48bf3fa
2 changed files with 1097 additions and 1 deletions

View File

@@ -463,7 +463,242 @@ interface UIStore {
## 6. Authentication Architecture
### 6.1 Token Management Strategy
### 6.1 Context-Based Dependency Injection Pattern
**Architecture Overview:**
This project uses a **hybrid authentication pattern** combining Zustand for state management and React Context for dependency injection. This provides the best of both worlds:
```
Component → useAuth() hook → AuthContext → Zustand Store → Storage Layer → Crypto (AES-GCM)
Injectable for tests
Production: Real store | Tests: Mock store
```
**Why This Pattern?**
**Benefits:**
- **Testable**: E2E tests can inject mock stores without backend
- **Performant**: Zustand handles state efficiently, Context is just a thin wrapper
- **Type-safe**: Full TypeScript inference throughout
- **Maintainable**: Clear separation (Context = DI, Zustand = state)
- **Extensible**: Easy to add auth events, middleware, logging
- **React-idiomatic**: Follows React best practices
**Key Design Principles:**
1. **Thin Context Layer**: Context only provides dependency injection, no business logic
2. **Zustand for State**: All state management stays in Zustand (no duplicated state)
3. **Backward Compatible**: Internal refactor only, no API changes
4. **Type Safe**: Context interface exactly matches Zustand store interface
5. **Performance**: Context value is stable (no unnecessary re-renders)
### 6.2 Implementation Components
#### AuthContext Provider (`src/lib/auth/AuthContext.tsx`)
**Purpose**: Wraps Zustand store in React Context for dependency injection
```typescript
// Accepts optional store prop for testing
<AuthProvider store={mockStore}> // Unit tests
<App />
</AuthProvider>
// Or checks window global for E2E tests
window.__TEST_AUTH_STORE__ = mockStoreHook;
// Or uses production singleton (default)
<AuthProvider>
<App />
</AuthProvider>
```
**Implementation Details:**
- Stores Zustand hook function (not state) in Context
- Priority: explicit prop → E2E test store → production singleton
- Type-safe window global extension for E2E injection
- Calls hook internally (follows React Rules of Hooks)
#### useAuth Hook (Polymorphic)
**Supports two usage patterns:**
```typescript
// Pattern 1: Full state access (simple)
const { user, isAuthenticated } = useAuth();
// Pattern 2: Selector (optimized for performance)
const user = useAuth(state => state.user);
```
**Why Polymorphic?**
- Simple pattern for most use cases
- Optimized pattern available when needed
- Type-safe with function overloads
- No performance overhead
**Critical Implementation Detail:**
```typescript
export function useAuth(): AuthState;
export function useAuth<T>(selector: (state: AuthState) => T): T;
export function useAuth<T>(selector?: (state: AuthState) => T): AuthState | T {
const storeHook = useContext(AuthContext);
if (!storeHook) {
throw new Error("useAuth must be used within AuthProvider");
}
// CRITICAL: Call the hook internally (follows React Rules of Hooks)
return selector ? storeHook(selector) : storeHook();
}
```
**Do NOT** return the hook function itself - this violates React Rules of Hooks!
### 6.3 Usage Patterns
#### For Components (Rendering Auth State)
**Use `useAuth()` from Context:**
```typescript
import { useAuth } from '@/lib/stores';
function MyComponent() {
// Full state access
const { user, isAuthenticated } = useAuth();
// Or with selector for optimization
const user = useAuth(state => state.user);
if (!isAuthenticated) {
return <LoginPrompt />;
}
return <div>Hello, {user?.first_name}!</div>;
}
```
**Why?**
- Component re-renders when auth state changes
- Type-safe access to all state properties
- Clean, idiomatic React code
#### For Mutation Callbacks (Updating Auth State)
**Use `useAuthStore.getState()` directly:**
```typescript
import { useAuthStore } from '@/lib/stores/authStore';
export function useLogin() {
return useMutation({
mutationFn: async (data) => {
const response = await loginAPI(data);
// Access store directly in callback (outside render)
const setAuth = useAuthStore.getState().setAuth;
await setAuth(response.user, response.token);
},
});
}
```
**Why?**
- Event handlers run outside React render cycle
- Don't need to re-render when state changes
- Using `getState()` directly is cleaner
- Avoids unnecessary hook rules complexity
#### Admin-Only Features
```typescript
import { useAuth } from '@/lib/stores';
function AdminPanel() {
const user = useAuth(state => state.user);
const isAdmin = user?.is_superuser ?? false;
if (!isAdmin) {
return <AccessDenied />;
}
return <AdminDashboard />;
}
```
### 6.4 Testing Integration
#### Unit Tests (Jest)
```typescript
import { useAuth } from '@/lib/stores';
jest.mock('@/lib/stores', () => ({
useAuth: jest.fn(),
}));
test('renders user name', () => {
(useAuth as jest.Mock).mockReturnValue({
user: { first_name: 'John', last_name: 'Doe' },
isAuthenticated: true,
});
render(<MyComponent />);
expect(screen.getByText('John Doe')).toBeInTheDocument();
});
```
#### E2E Tests (Playwright)
```typescript
import { test, expect } from '@playwright/test';
test.describe('Protected Pages', () => {
test.beforeEach(async ({ page }) => {
// Inject mock store before navigation
await page.addInitScript(() => {
(window as any).__TEST_AUTH_STORE__ = () => ({
user: { id: '1', email: 'test@example.com', first_name: 'Test', last_name: 'User' },
accessToken: 'mock-token',
refreshToken: 'mock-refresh',
isAuthenticated: true,
isLoading: false,
tokenExpiresAt: Date.now() + 900000,
});
});
});
test('should display user profile', async ({ page }) => {
await page.goto('/settings/profile');
// No redirect to login - authenticated via mock
await expect(page).toHaveURL('/settings/profile');
await expect(page.locator('input[name="email"]')).toHaveValue('test@example.com');
});
});
```
### 6.5 Provider Tree Structure
**Correct Order** (Critical for Functionality):
```typescript
// src/app/layout.tsx
<AuthProvider> {/* 1. Provides auth DI layer */}
<AuthInitializer /> {/* 2. Loads auth from storage (needs AuthProvider) */}
<Providers> {/* 3. Other providers (Theme, Query) */}
{children}
</Providers>
</AuthProvider>
```
**Why This Order?**
- AuthProvider must wrap AuthInitializer (AuthInitializer uses auth state)
- AuthProvider should wrap all app providers (auth available everywhere)
- Keep provider tree shallow for performance
### 6.6 Token Management Strategy
**Two-Token System:**
- **Access Token**: Short-lived (15 min), stored in memory/sessionStorage