Back to list
CodySwannGT

container-view-pattern

by CodySwannGT

Claude Code governance framework - guardrails, guidance, and automated enforcement for AI-assisted development

1🍴 1📅 Jan 24, 2026

SKILL.md


name: container-view-pattern description: This skill enforces the Container/View pattern for React components. It should be used when creating new components, validating existing components, or refactoring components to follow the separation of concerns pattern where Container handles logic and View handles presentation.

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:

DirectoryAppliesNotes
features/*/components/YesAll feature components
features/*/screens/YesAll feature screens
components/YesShared components
screens/YesShared screens
components/ui/NoUI primitives (GlueStack)
components/shared/NoSimple shared utilities
components/icons/NoIcon 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:

TypeCommandCreates in
Global componentcreate_component.py global-component PlayerCardcomponents/PlayerCard/
Feature componentcreate_component.py feature-component PlayerCard player-kanbanfeatures/player-kanban/components/PlayerCard/
Global screencreate_component.py global-screen Settingsscreens/Settings/
Feature screencreate_component.py feature-screen Main dashboardfeatures/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:

RuleDescription
component-structure/enforce-component-structureValidates directory structure and file naming
component-structure/no-return-in-viewEnsures View uses arrow shorthand
component-structure/require-memo-in-viewEnsures View uses memo and displayName
component-structure/single-component-per-fileOne 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

IssueResolution
Rendering UI elements besides ViewContainer must ONLY return the corresponding View component
Rendering multiple componentsMove all UI to View; Container returns only View
Missing useMemo for objects/arraysWrap computed values in useMemo
Missing useCallback for functionsWrap handlers in useCallback
Logic in View componentMove logic to Container
Inline function propsCreate memoized handler

View Violations

IssueResolution
Using block body { return }Convert to arrow shorthand () => (...)
Missing memo wrapperAdd export default memo(ComponentView)
Missing displayNameAdd ComponentView.displayName = "ComponentView"
Contains hooksMove hooks to Container
Contains stateMove 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

Score

Total Score

70/100

Based on repository quality metrics

SKILL.md

SKILL.mdファイルが含まれている

+20
LICENSE

ライセンスが設定されている

+10
説明文

100文字以上の説明がある

+10
人気

GitHub Stars 100以上

0/15
最近の活動

3ヶ月以内に更新がある

0/10
フォーク

10回以上フォークされている

0/5
Issue管理

オープンIssueが50未満

+5
言語

プログラミング言語が設定されている

+5
タグ

1つ以上のタグが設定されている

0/5

Reviews

💬

Reviews coming soon