- Added centralized exports for video streaming components and hooks. - Implemented `useVideoInfo` hook for fetching and managing video metadata and streaming information. - Developed `useVideoList` hook for managing video list state, fetching, filtering, and pagination. - Created `useVideoPlayer` hook for managing video player state and controls. - Established `videoApiService` for handling API interactions related to video streaming. - Defined TypeScript types for video streaming feature, including video metadata, API responses, and component props. - Added utility functions for video operations, formatting, and data processing. - Created main entry point for the video streaming feature, exporting all public APIs.
7.8 KiB
7.8 KiB
🏗️ Modular Architecture Guide
This guide demonstrates the modular architecture patterns implemented in the video streaming feature and how to apply them to other parts of the project.
🎯 Goals
- Separation of Concerns: Each module has a single responsibility
- Reusability: Components can be used across different parts of the application
- Maintainability: Easy to understand, modify, and test individual pieces
- Scalability: Easy to add new features without affecting existing code
📁 Feature-Based Structure
src/features/video-streaming/
├── components/ # UI Components
│ ├── VideoPlayer.tsx
│ ├── VideoCard.tsx
│ ├── VideoList.tsx
│ ├── VideoModal.tsx
│ ├── VideoThumbnail.tsx
│ └── index.ts
├── hooks/ # Custom React Hooks
│ ├── useVideoList.ts
│ ├── useVideoPlayer.ts
│ ├── useVideoInfo.ts
│ └── index.ts
├── services/ # API & Business Logic
│ └── videoApi.ts
├── types/ # TypeScript Definitions
│ └── index.ts
├── utils/ # Pure Utility Functions
│ └── videoUtils.ts
├── VideoStreamingPage.tsx # Main Feature Page
└── index.ts # Feature Export
🧩 Layer Responsibilities
1. Components Layer (/components)
- Purpose: Pure UI components that handle rendering and user interactions
- Rules:
- No direct API calls
- Receive data via props
- Emit events via callbacks
- Minimal business logic
Example:
// ✅ Good: Pure component with clear props
export const VideoCard: React.FC<VideoCardProps> = ({
video,
onClick,
showMetadata = true,
}) => {
return (
<div onClick={() => onClick?.(video)}>
{/* UI rendering */}
</div>
);
};
// ❌ Bad: Component with API calls
export const VideoCard = () => {
const [video, setVideo] = useState(null);
useEffect(() => {
fetch('/api/videos/123').then(/* ... */); // Don't do this!
}, []);
};
2. Hooks Layer (/hooks)
- Purpose: Manage state, side effects, and provide data to components
- Rules:
- Handle API calls and data fetching
- Manage component state
- Provide clean interfaces to components
Example:
// ✅ Good: Hook handles complexity, provides simple interface
export function useVideoList(options = {}) {
const [videos, setVideos] = useState([]);
const [loading, setLoading] = useState(false);
const fetchVideos = useCallback(async () => {
setLoading(true);
try {
const data = await videoApiService.getVideos();
setVideos(data.videos);
} finally {
setLoading(false);
}
}, []);
return { videos, loading, refetch: fetchVideos };
}
3. Services Layer (/services)
- Purpose: Handle external dependencies (APIs, storage, etc.)
- Rules:
- Pure functions or classes
- No React dependencies
- Handle errors gracefully
- Provide consistent interfaces
Example:
// ✅ Good: Service handles API complexity
export class VideoApiService {
async getVideos(params = {}) {
try {
const response = await fetch(this.buildUrl('/videos', params));
return await this.handleResponse(response);
} catch (error) {
throw new VideoApiError('FETCH_ERROR', error.message);
}
}
}
4. Types Layer (/types)
- Purpose: Centralized TypeScript definitions
- Rules:
- Define all interfaces and types
- Export from index.ts
- Keep types close to their usage
5. Utils Layer (/utils)
- Purpose: Pure utility functions
- Rules:
- No side effects
- Easily testable
- Single responsibility
🔄 Component Composition Patterns
Small, Focused Components
Instead of large monolithic components, create small, focused ones:
// ✅ Good: Small, focused components
<VideoList>
{videos.map(video => (
<VideoCard key={video.id} video={video} onClick={onVideoSelect} />
))}
</VideoList>
// ❌ Bad: Monolithic component
<VideoSystemPage>
{/* 500+ lines of mixed concerns */}
</VideoSystemPage>
Composition over Inheritance
// ✅ Good: Compose features
export const VideoStreamingPage = () => {
const { videos, loading } = useVideoList();
const [selectedVideo, setSelectedVideo] = useState(null);
return (
<div>
<VideoList videos={videos} onVideoSelect={setSelectedVideo} />
<VideoModal video={selectedVideo} />
</div>
);
};
🎨 Applying to Existing Components
Example: Breaking Down VisionSystem Component
Current Structure (Monolithic):
// ❌ Current: One large component
export const VisionSystem = () => {
// 900+ lines of mixed concerns
return (
<div>
{/* System status */}
{/* Camera cards */}
{/* Storage info */}
{/* MQTT status */}
</div>
);
};
Proposed Modular Structure:
src/features/vision-system/
├── components/
│ ├── SystemStatusCard.tsx
│ ├── CameraCard.tsx
│ ├── CameraGrid.tsx
│ ├── StorageOverview.tsx
│ ├── MqttStatus.tsx
│ └── index.ts
├── hooks/
│ ├── useSystemStatus.ts
│ ├── useCameraList.ts
│ └── index.ts
├── services/
│ └── visionApi.ts
└── VisionSystemPage.tsx
Refactored Usage:
// ✅ Better: Composed from smaller parts
export const VisionSystemPage = () => {
return (
<div>
<SystemStatusCard />
<CameraGrid />
<StorageOverview />
<MqttStatus />
</div>
);
};
// Now you can reuse components elsewhere:
export const DashboardHome = () => {
return (
<div>
<SystemStatusCard /> {/* Reused! */}
<QuickStats />
</div>
);
};
📋 Migration Strategy
Phase 1: Extract Utilities
- Move pure functions to
/utils - Move types to
/types - Create service classes for API calls
Phase 2: Extract Hooks
- Create custom hooks for data fetching
- Move state management to hooks
- Simplify component logic
Phase 3: Break Down Components
- Identify distinct UI sections
- Extract to separate components
- Use composition in parent components
Phase 4: Feature Organization
- Group related components, hooks, and services
- Create feature-level exports
- Update imports across the application
🧪 Testing Benefits
Modular architecture makes testing much easier:
// ✅ Easy to test individual pieces
describe('VideoCard', () => {
it('displays video information', () => {
render(<VideoCard video={mockVideo} />);
expect(screen.getByText(mockVideo.filename)).toBeInTheDocument();
});
});
describe('useVideoList', () => {
it('fetches videos on mount', async () => {
const { result } = renderHook(() => useVideoList());
await waitFor(() => {
expect(result.current.videos).toHaveLength(3);
});
});
});
🚀 Benefits Achieved
- Reusability:
VideoCardcan be used in lists, grids, or modals - Maintainability: Each file has a single, clear purpose
- Testability: Small, focused units are easy to test
- Developer Experience: Clear structure makes onboarding easier
- Performance: Smaller components enable better optimization
📝 Best Practices
- Start Small: Begin with one feature and apply patterns gradually
- Single Responsibility: Each file should have one clear purpose
- Clear Interfaces: Use TypeScript to define clear contracts
- Consistent Naming: Follow naming conventions across features
- Documentation: Document complex logic and interfaces
This modular approach transforms large, hard-to-maintain components into small, reusable, and testable pieces that can be composed together to create powerful features.