返回 Skill 列表
extension
分类: 开发与工程无需 API Key

container-view-pattern

此技能强制React组件遵循容器/视图模式。在创建新组件、验证现有组件或重构组件以遵循关注点分离模式(其中容器处理逻辑,视图处理展示)时应使用此技能。

person作者: jakexiaohubgithub

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:

  1. Single View render: Container must ONLY render its corresponding View component - no other UI elements or components
  2. State management: Use useState, useReducer
  3. Data fetching: Use GraphQL hooks, API calls
  4. Memoization: Wrap all computed values in useMemo
  5. Event handlers: Wrap all handlers in useCallback with proper dependencies
  6. Formatting: All data transformation and formatting logic
  7. 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:

  1. Arrow function shorthand: Use () => (...) not () => { return (...); }
  2. No return statements: The component body must be a single JSX expression
  3. memo wrapper: Export with memo() for performance optimization
  4. displayName: Set ComponentName.displayName = "ComponentName"
  5. Readonly props: All props should be marked as readonly
  6. No hooks: View should not contain useState, useEffect, useMemo, etc.
  7. 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 bun with 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-patterns
  • references/examples.md - Complete component examples