# Container/Presentation Pattern ## Overview The Container/Presentation pattern separates component logic from presentation, improving testability, reusability, and maintainability. This pattern is now the standard for complex components in this codebase. ## Motivation ### Benefits - **Testability**: Pure presentation components can be tested independently with simple prop assertions - **Reusability**: Presentation components can be reused with different data sources or logic - **Maintainability**: Clear separation makes it easier to locate and modify specific concerns - **Performance**: Easier to optimize rendering with React.memo on pure components ### When to Use Use this pattern for components that have: - Business logic or state management - Data fetching or API calls - Analytics tracking - Complex event handlers - Custom hooks usage - Dynamic imports or side effects Simple presentational components (e.g., `Button`, `Avatar`) can remain as single files. ## Folder Structure Each component following this pattern should have this structure: ``` app/components/[ComponentName]/ ├── index.tsx # Exports container as default ├── [ComponentName].container.tsx # Logic, hooks, state management ├── [ComponentName].view.tsx # Pure presentation component └── [ComponentName].types.ts # Shared TypeScript types ``` ### File Responsibilities #### `index.tsx` - Exports the container component as the default export - Optionally exports types for external use - Maintains backward compatibility with existing import paths ```typescript export { default } from "./AskOrganizer.container"; export type { AskOrganizerProps } from "./AskOrganizer.types"; ``` #### `[ComponentName].container.tsx` **Contains all logic:** - React hooks (`useState`, `useEffect`, custom hooks) - Event handlers and business logic - Data fetching and API calls - Analytics tracking - State management - Computed values and derived state - Side effects **Should NOT contain:** - JSX layout details (beyond composing the view) - Inline styles or complex className logic (pass as props) - Direct DOM manipulation ```typescript "use client"; import { memo } from "react"; import { useAnalytics } from "../../hooks"; import { AskOrganizerView } from "./AskOrganizer.view"; import type { AskOrganizerProps } from "./AskOrganizer.types"; function AskOrganizerContainer(props: AskOrganizerProps) { const { trackEvent } = useAnalytics(); const handleContactClick = () => { trackEvent({ event: "contact_button_click", category: "engagement", component: "AskOrganizer", }); // ... additional logic }; // Compute derived props const variantStyles = computeVariantStyles(props.variant); return ( ); } export default memo(AskOrganizerContainer); ``` #### `[ComponentName].view.tsx` **Pure presentation:** - Receives all data via props - Renders JSX based on props - No hooks, no state, no side effects - Only imports other presentational components **Should NOT contain:** - `useState`, `useEffect`, or any hooks - Event handler implementations (receive as callbacks) - Data fetching or API calls - Analytics tracking - Business logic ```typescript import ContentLockup from "../ContentLockup"; import Button from "../Button"; import type { AskOrganizerViewProps } from "./AskOrganizer.types"; export function AskOrganizerView({ title, subtitle, description, buttonText, buttonHref, variant, onContactClick, variantStyles, ...props }: AskOrganizerViewProps) { return (
); } ``` #### `[ComponentName].types.ts` - Shared TypeScript interfaces and types - Public props interface (used by consumers) - Internal view props (used between container and view) - Any utility types specific to the component ```typescript export interface AskOrganizerProps { title?: string; subtitle?: string; buttonText?: string; buttonHref?: string; variant?: "centered" | "left-aligned" | "compact" | "inverse"; onContactClick?: (data: ContactClickData) => void; } export interface AskOrganizerViewProps extends AskOrganizerProps { onContactClick: () => void; variantStyles: { container: string; buttonContainer: string; }; } ``` ## Rules of Thumb ### Container Components ✅ **DO:** - Use React hooks (`useState`, `useEffect`, custom hooks) - Handle all event handlers and business logic - Fetch data and manage loading states - Track analytics events - Compute derived values from props/state - Compose the view component with computed props ❌ **DON'T:** - Include complex JSX layout (delegate to view) - Mix presentation logic with business logic - Access DOM directly (use refs when necessary) ### View Components ✅ **DO:** - Receive all data via props - Render JSX based on props - Import only presentational components - Use simple conditional rendering - Accept callback props for user interactions ❌ **DON'T:** - Use any React hooks - Manage state or side effects - Fetch data or make API calls - Track analytics directly - Implement business logic - Access browser APIs directly ## Example: AskOrganizer ### Before (Monolithic) ```typescript "use client"; import { memo } from "react"; import { useAnalytics } from "../hooks"; import ContentLockup from "./ContentLockup"; import Button from "./Button"; const AskOrganizer = memo(({ title, variant, ...props }) => { const { trackEvent } = useAnalytics(); const handleContactClick = () => { trackEvent({ event: "contact_click", component: "AskOrganizer" }); }; return (
); }); ``` ### After (Container/Presentation) **AskOrganizer.container.tsx:** ```typescript "use client"; import { memo } from "react"; import { useAnalytics } from "../../hooks"; import { AskOrganizerView } from "./AskOrganizer.view"; import type { AskOrganizerProps } from "./AskOrganizer.types"; function AskOrganizerContainer(props: AskOrganizerProps) { const { trackEvent } = useAnalytics(); const handleContactClick = () => { trackEvent({ event: "contact_click", component: "AskOrganizer" }); }; return ; } export default memo(AskOrganizerContainer); ``` **AskOrganizer.view.tsx:** ```typescript import ContentLockup from "../ContentLockup"; import Button from "../Button"; import type { AskOrganizerViewProps } from "./AskOrganizer.types"; export function AskOrganizerView({ title, onContactClick, ...props }: AskOrganizerViewProps) { return (
); } ``` ## Migration Checklist When converting an existing component to this pattern: - [ ] **Identify separation points** - [ ] List all hooks and state management - [ ] List all event handlers and business logic - [ ] List all data fetching and side effects - [ ] Identify pure presentation JSX - [ ] **Create folder structure** - [ ] Create `[ComponentName]/` folder - [ ] Create `[ComponentName].types.ts` with shared types - [ ] Create `[ComponentName].view.tsx` with pure presentation - [ ] Create `[ComponentName].container.tsx` with all logic - [ ] Create `index.tsx` exporting container - [ ] **Extract types** - [ ] Move component props interface to `types.ts` - [ ] Create view props interface extending container props - [ ] Export types from `index.tsx` for external use - [ ] **Move presentation to view** - [ ] Copy JSX to view component - [ ] Remove all hooks and state - [ ] Replace event handlers with callback props - [ ] Replace computed values with props - [ ] Ensure view is a pure function - [ ] **Move logic to container** - [ ] Move all hooks to container - [ ] Move event handlers to container - [ ] Move data fetching to container - [ ] Compute derived props in container - [ ] Render view component with computed props - [ ] **Update exports** - [ ] Export container as default from `index.tsx` - [ ] Export types from `index.tsx` - [ ] Delete original component file - [ ] Verify import paths still work - [ ] **Update tests** - [ ] Verify tests still pass (imports should resolve automatically) - [ ] Update any tests that relied on implementation details - [ ] Add tests for view component with mocked props if needed - [ ] **Update Storybook** - [ ] Verify stories still work (imports should resolve automatically) - [ ] Optionally add view-only stories with mocked props ## Refactored Components The following components have been refactored to use this pattern: - ✅ **AskOrganizer** - Analytics tracking and event handlers - ✅ **NumberedCards** - Schema generation with `useSchemaData` hook - ✅ **FeatureGrid** - Memoized feature data structures - ✅ **WebVitalsDashboard** - Dynamic imports, data fetching, complex state - ✅ **Select** - Complex form state management with refs and keyboard navigation These components serve as reference implementations for the pattern. ## Remaining Components The following components are candidates for future conversion: ### High Priority (Complex Logic) - `Header` / `HomeHeader` - Navigation state, conditional rendering logic - `MenuBar` - Menu state management, keyboard navigation - `ContextMenu` - Positioning logic, click outside handling - `RadioGroup` - Group state management - `ToggleGroup` - Group state management ### Medium Priority (Some Logic) - `ContentContainer` - Data fetching or transformation - `RelatedArticles` - Data fetching, filtering logic - `RuleStack` - Complex rendering logic - `LogoWall` - Animation or interaction logic ### Low Priority (Mostly Presentational) - `Button`, `Avatar`, `Checkbox`, `Input`, `TextArea` - Simple presentational components - `Separator`, `SectionHeader`, `SectionNumber` - Pure presentation - `QuoteBlock`, `QuoteDecor`, `HeroDecor` - Decorative components ## Best Practices 1. **Start with complex components** - Components with the most logic benefit most from separation 2. **Keep it simple** - Don't over-engineer simple presentational components 3. **Maintain backward compatibility** - Import paths should remain unchanged 4. **Test both layers** - Test container for logic, view for presentation 5. **Document the pattern** - Add comments explaining non-obvious prop flows 6. **Use TypeScript strictly** - Leverage types to enforce the separation ## Additional Resources - [React Container/Presenter Pattern](https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0) - [Separation of Concerns in React](https://react.dev/learn/thinking-in-react) --- **Last Updated**: April 2025 **Maintained by**: CommunityRule Development Team