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, submission
  • BasicInfoSection.tsx - Title, category, duration inputs
  • AudienceSection.tsx - Target audience, difficulty
  • LearningObjectivesSection.tsx - Add/remove objectives
  • ReferenceMaterialsSection.tsx - PDF upload, URLs
  • GenerationOptionsSection.tsx - Async, video toggles
  • CourseGenerationForm.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 cards
  • CourseProgressSection.tsx - Individual course progress
  • ActivityChartSection.tsx - Weekly activity visualization

LessonForm

Location: frontend/components/admin/LessonForm.tsx

Hook: frontend/hooks/useLessonForm.ts

Sections:

  • LessonBasicFields.tsx - Title, day, order
  • LessonContentEditor.tsx - Rich text content
  • LessonResourcesSection.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 info
  • PreferencesSection.tsx - User preferences
  • RoleSection.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:

  1. Identify Sections: Look for distinct UI areas with clear boundaries
  2. Extract Hook: Move all state and handlers to a custom hook
  3. Create Section Components: Extract each section as a pure component
  4. Update Main Component: Wire sections together with hook data
  5. Add Tests: Test hook and sections independently

Back to top

Momentum LMS © 2025. Distributed under the MIT license.