Container/View Pattern
This skill provides guidance and validation for the Container/View component pattern used in this codebase.
Pattern Overview
The Container/View pattern separates components into two distinct files:
- Container (
*Container.tsx): Handles logic, state, API calls, data fetching, and event handlers - View (
*View.tsx): Handles rendering UI only, receiving all data and callbacks as props - Index (
index.tsx): Exports the Container as the default component
Where This Pattern Applies
The Container/View pattern is required in these directories:
| Directory | Applies | Notes |
| ------------------------ | ------- | ------------------------- |
| features/*/components/ | Yes | All feature components |
| features/*/screens/ | Yes | All feature screens |
| components/ | Yes | Shared components |
| screens/ | Yes | Shared screens |
| components/ui/ | No | UI primitives (GlueStack) |
| components/shared/ | No | Simple shared utilities |
| components/icons/ | No | Icon components |
When to Use This Skill
- Creating a new component in any of the directories above
- Validating that existing components follow the pattern
- Refactoring a component to follow the pattern
- Reviewing code for pattern compliance
Creating a New Component
Option 1: Use the Skill Generator Script
Run the skill's generator script for any component type:
python3 .claude/skills/container-view-pattern/scripts/create_component.py <type> <name> [feature]
Component Types:
| Type | Command | Creates in |
| ----------------- | ---------------------------------------------------------------- | ----------------------------------------------- |
| Global component | create_component.py global-component PlayerCard | components/PlayerCard/ |
| Feature component | create_component.py feature-component PlayerCard player-kanban | features/player-kanban/components/PlayerCard/ |
| Global screen | create_component.py global-screen Settings | screens/Settings/ |
| Feature screen | create_component.py feature-screen Main dashboard | features/dashboard/screens/Main/ |
Option 2: Manual Creation
Create the following directory structure:
ComponentName/
├── ComponentNameContainer.tsx
├── ComponentNameView.tsx
└── index.tsx
Container Component Requirements
Container components handle all business logic:
- Single View render: Container must ONLY render its corresponding View component - no other UI elements or components
- State management: Use
useState,useReducer - Data fetching: Use GraphQL hooks, API calls
- Memoization: Wrap all computed values in
useMemo - Event handlers: Wrap all handlers in
useCallbackwith proper dependencies - Formatting: All data transformation and formatting logic
- Conditional logic: Determine what state to pass to View (loading, error, empty flags)
Container Code Order (enforced by ESLint)
Containers must follow this specific order:
const ExampleContainer = () => {
// 1. Variables, state, useMemo, useCallback (same group)
const [state, setState] = useState();
const computed = useMemo(() => state * 2, [state]);
const handleClick = useCallback(() => {}, []);
// 2. useEffect hooks
useEffect(() => {
// side effects
}, []);
// 3. Return statement (always last)
return <ExampleView />;
};
Container Template
import { useCallback, useMemo, useState } from "react";
import ComponentNameView from "./ComponentNameView";
/**
* Props for the ComponentName component.
*/
interface ComponentNameProps {
readonly id: string;
}
/**
* Container component that manages state and logic for ComponentName.
* @param props - Component properties
* @param props.id - The unique identifier
*/
const ComponentNameContainer = ({ id }: ComponentNameProps) => {
// State
const [isLoading, setIsLoading] = useState(false);
// Memoized computed values
const formattedData = useMemo(() => {
return data?.toUpperCase() ?? "";
}, [data]);
// Event handlers wrapped in useCallback
const handleSubmit = useCallback(() => {
setIsLoading(true);
}, []);
return (
<ComponentNameView
formattedData={formattedData}
isLoading={isLoading}
onSubmit={handleSubmit}
/>
);
};
export default ComponentNameContainer;
View Component Requirements
View components are pure presentation:
- Arrow function shorthand: Use
() => (...)not() => { return (...); } - No return statements: The component body must be a single JSX expression
- memo wrapper: Export with
memo()for performance optimization - displayName: Set
ComponentName.displayName = "ComponentName" - Readonly props: All props should be marked as
readonly - No hooks: View should not contain
useState,useEffect,useMemo, etc. - No logic: All conditional rendering should use ternary expressions in JSX
View Template
import { memo } from "react";
import { Box } from "@/components/ui/box";
import { Text } from "@/components/ui/text";
/**
* Props for the ComponentNameView component.
*/
interface ComponentNameViewProps {
readonly formattedData: string;
readonly isLoading: boolean;
readonly onSubmit: () => void;
}
/**
* View component that renders the ComponentName UI.
* @param props - Component properties
* @param props.formattedData - Pre-formatted display data
* @param props.isLoading - Loading state indicator
* @param props.onSubmit - Submit handler callback
*/
const ComponentNameView = ({
formattedData,
isLoading,
onSubmit,
}: ComponentNameViewProps) => (
<Box testID="COMPONENT_NAME.CONTAINER">
{isLoading ? <Text>Loading...</Text> : <Text>{formattedData}</Text>}
</Box>
);
ComponentNameView.displayName = "ComponentNameView";
export default memo(ComponentNameView);
Index File
Export the Container as the default:
export { default } from "./ComponentNameContainer";
Validation
ESLint Rules
The following ESLint rules enforce the pattern:
| Rule | Description |
| ------------------------------------------------- | --------------------------------------------- |
| component-structure/enforce-component-structure | Validates directory structure and file naming |
| component-structure/no-return-in-view | Ensures View uses arrow shorthand |
| component-structure/require-memo-in-view | Ensures View uses memo and displayName |
| component-structure/single-component-per-file | One component per file |
Manual Validation
Run the validation script to check a component:
python3 .claude/skills/container-view-pattern/scripts/validate_component.py <path-to-component-directory>
Run ESLint to check all components:
bun run lint
Note: Replace
bunwith your project's package manager (npm,yarn,pnpm) as needed.
Common Violations
Container Violations
| Issue | Resolution |
| ------------------------------------ | ----------------------------------------------------------- |
| Rendering UI elements besides View | Container must ONLY return the corresponding View component |
| Rendering multiple components | Move all UI to View; Container returns only View |
| Missing useMemo for objects/arrays | Wrap computed values in useMemo |
| Missing useCallback for functions | Wrap handlers in useCallback |
| Logic in View component | Move logic to Container |
| Inline function props | Create memoized handler |
View Violations
| Issue | Resolution |
| ----------------------------- | ------------------------------------------------- |
| Using block body { return } | Convert to arrow shorthand () => (...) |
| Missing memo wrapper | Add export default memo(ComponentView) |
| Missing displayName | Add ComponentView.displayName = "ComponentView" |
| Contains hooks | Move hooks to Container |
| Contains state | Move state to Container |
Extracting Helper Functions
When View components exceed ESLint's cognitive complexity threshold (28), extract render helper functions. For simple cases, prefer inline JSX:
/**
* Renders the loading skeleton state.
* @param props - Helper function properties
* @param props.isDark - Whether dark mode is active
*/
function renderLoadingState(props: { readonly isDark: boolean }) {
const { isDark } = props;
return <LoadingSkeleton isDark={isDark} />;
}
const ComponentView = ({ isLoading, isDark }: Props) => (
<Box>{isLoading ? renderLoadingState({ isDark }) : <Content />}</Box>
);
Event Handler Naming Convention
- Container: Use
handle*prefix (e.g.,handleSubmit,handleClick) - View props: Use
on*prefix (e.g.,onSubmit,onClick)
// Container
const handleSubmit = useCallback(() => { ... }, []);
return <ComponentView onSubmit={handleSubmit} />;
// View
const ComponentView = ({ onSubmit }: Props) => (
<Button onPress={onSubmit}>Submit</Button>
);
Reference Documentation
For detailed examples and edge cases, read:
references/patterns.md- Common patterns and anti-patternsreferences/examples.md- Complete component examples