Component Patterns Guide
Overview
This guide documents the patterns used for React components in the Momentum codebase.
Pattern: Hook + Sections
For components over 200 lines, we use the Hook + Sections pattern.
Structure
components/
feature-name/
FeatureComponent.tsx # Main component (~150 lines)
sections/
SectionA.tsx # Presentational section
SectionB.tsx # Presentational section
index.ts # Barrel export
hooks/
useFeatureComponent.ts # State and logic hook
Example: CourseGenerationForm
Before: Single 673-line component
After:
useCourseGenerationForm.ts- Form state, validation, submissionBasicInfoSection.tsx- Title, category, duration inputsAudienceSection.tsx- Target audience, difficultyLearningObjectivesSection.tsx- Add/remove objectivesReferenceMaterialsSection.tsx- PDF upload, URLsGenerationOptionsSection.tsx- Async, video togglesCourseGenerationForm.tsx- Composition (~150 lines)
When to Apply
Apply this pattern when:
- Component exceeds 200 lines
- Component has complex state management
- Multiple distinct UI sections exist
- Testing the component is difficult
Hook Conventions
// Naming: use{ComponentName}
export function useCourseForm(initialData?: Course) {
// Return object with:
return {
// State
formData,
isSubmitting,
error,
// Handlers
handleChange,
handleSubmit,
// Derived values
isValid,
isDirty,
};
}
Section Component Conventions
// Props include what the section needs to render and handle events
interface BasicInfoSectionProps {
formData: CourseFormData;
onChange: (field: keyof CourseFormData, value: unknown) => void;
categories: Category[];
isLoading?: boolean;
}
// Sections are pure presentational components
export function BasicInfoSection({
formData,
onChange,
categories,
isLoading,
}: BasicInfoSectionProps) {
return (
<div className="...">
{/* Section UI */}
</div>
);
}
API Client Pattern
All API calls go through the centralized client:
// Good - uses centralized client
import { apiClient } from '@/lib/api/api-client';
const data = await apiClient.get('/endpoint');
// Bad - duplicates auth/error handling
const response = await fetch('/endpoint', {
headers: { Authorization: `Bearer ${token}` },
});
File Organization
lib/
api/
api-client.ts # Centralized HTTP client
api-types.ts # Shared types
courses.ts # Course API functions
lessons.ts # Lesson API functions
...
auth/
index.ts # Public API
cognito-config.ts # Configuration
sign-in.ts # Sign in/out
sign-up.ts # Registration
session.ts # Session management
password.ts # Password operations
social-login.ts # OAuth
Component Examples
ProgressTab
Location: frontend/components/dashboard/ProgressTab.tsx
Hook: frontend/hooks/useProgressStats.ts
Sections:
ProgressStatsSection.tsx- Overall statistics cardsCourseProgressSection.tsx- Individual course progressActivityChartSection.tsx- Weekly activity visualization
LessonForm
Location: frontend/components/admin/LessonForm.tsx
Hook: frontend/hooks/useLessonForm.ts
Sections:
LessonBasicFields.tsx- Title, day, orderLessonContentEditor.tsx- Rich text contentLessonResourcesSection.tsx- Resources and action items
Admin User Edit
Location: frontend/app/admin/users/[id]/edit/page.tsx
Hook: frontend/hooks/useUserEditForm.ts
Sections:
ProfileSection.tsx- Basic profile infoPreferencesSection.tsx- User preferencesRoleSection.tsx- Role management
Best Practices
1. Keep Hooks Focused
Each hook should handle one concern:
- Form state and validation
- Data fetching
- Business logic
2. Props Over Context for Sections
Pass props directly to section components rather than using context:
// Good - explicit dependencies
<BasicInfoSection formData={formData} onChange={handleChange} />
// Avoid - hidden dependencies
<BasicInfoSection /> // Uses context internally
3. Barrel Exports for Sections
Use index.ts for clean imports:
// sections/index.ts
export { BasicInfoSection } from './BasicInfoSection';
export { MetadataSection } from './MetadataSection';
// Usage
import { BasicInfoSection, MetadataSection } from './sections';
4. Consistent Return Types
Hooks should return consistent object shapes:
interface UseFormReturn<T> {
formData: T;
isSubmitting: boolean;
error: string | null;
handleChange: (field: keyof T, value: unknown) => void;
handleSubmit: (e: FormEvent) => Promise<void>;
isValid: boolean;
isDirty: boolean;
reset: () => void;
}
5. Error Boundaries
Wrap complex components in error boundaries:
function CourseForm() {
return (
<ErrorBoundary fallback={<FormErrorFallback />}>
<CourseFormContent />
</ErrorBoundary>
);
}
Migration Guide
To refactor an existing large component:
- Identify Sections: Look for distinct UI areas with clear boundaries
- Extract Hook: Move all state and handlers to a custom hook
- Create Section Components: Extract each section as a pure component
- Update Main Component: Wire sections together with hook data
- Add Tests: Test hook and sections independently