From lisa-expo
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.
npx claudepluginhub codyswanngt/lisa --plugin lisa-expoThis skill uses the workspace's default tool permissions.
This skill provides guidance and validation for the Container/View component pattern used in this codebase.
Generates design tokens/docs from CSS/Tailwind/styled-components codebases, audits visual consistency across 10 dimensions, detects AI slop in UI.
Records polished WebM UI demo videos of web apps using Playwright with cursor overlay, natural pacing, and three-phase scripting. Activates for demo, walkthrough, screen recording, or tutorial requests.
Delivers idiomatic Kotlin patterns for null safety, immutability, sealed classes, coroutines, Flows, extensions, DSL builders, and Gradle DSL. Use when writing, reviewing, refactoring, or designing Kotlin code.
This skill provides guidance and validation for the Container/View component pattern used in this codebase.
The Container/View pattern separates components into two distinct files:
*Container.tsx): Handles logic, state, API calls, data fetching, and event handlers*View.tsx): Handles rendering UI only, receiving all data and callbacks as propsindex.tsx): Exports the Container as the default componentThe 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 |
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/ |
Create the following directory structure:
ComponentName/
├── ComponentNameContainer.tsx
├── ComponentNameView.tsx
└── index.tsx
Container components handle all business logic:
useState, useReduceruseMemouseCallback with proper dependenciesContainers 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 />;
};
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 components are pure presentation:
() => (...) not () => { return (...); }memo() for performance optimizationComponentName.displayName = "ComponentName"readonlyuseState, useEffect, useMemo, etc.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);
Export the Container as the default:
export { default } from "./ComponentNameContainer";
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 |
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.
| 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 |
| 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 |
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>
);
handle* prefix (e.g., handleSubmit, handleClick)on* prefix (e.g., onSubmit, onClick)// Container
const handleSubmit = useCallback(() => { ... }, []);
return <ComponentView onSubmit={handleSubmit} />;
// View
const ComponentView = ({ onSubmit }: Props) => (
<Button onPress={onSubmit}>Submit</Button>
);
For detailed examples and edge cases, read:
references/patterns.md - Common patterns and anti-patternsreferences/examples.md - Complete component examples