plugins/lisa-expo/skills/container-view-pattern/SKILL.md
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 skillsauth add codyswanngt/lisa container-view-patternInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
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 examplesdevelopment
Prepare a machine — a fresh laptop or a throwaway container — to run coding agents, before any repository exists. Detects which of Lisa's supported agents (Claude Code, Codex, Cursor, OpenCode, Antigravity, Copilot) are already installed, asks which credential manager the machine uses (Bitwarden, 1Password, Doppler, Vault, AWS, or none), and installs only what is missing, each by its vendor's own preferred method. Idempotent, headless by default, and emits a Dockerfile for a spin-up/spin-down environment. Run it on a new machine, in a container, or before cloning anything.
tools
Provision and verify a remote execution environment for a host project — Codex Cloud today, other remote surfaces as they are added. Generates a repository-owned setup script that installs the declared toolchain, materializes secrets through lisa-secrets-access, and runs the project's own hook. Provisions by API where one exists, by driving the vendor console where one does not, and by emitting exact config otherwise — then proves the result with the same read-back regardless of which tier did the work. Use before dispatching any work with executionEnv.
tools
Bring a developer's machine in line with the toolchain the project declares. Reports every tool in remoteEnv.tools that is missing, outdated, or unpinned for this platform, and installs the missing ones into ~/.local/bin from the same pinned, checksummed entries the remote surfaces use — but only when asked. Same manifest, same pins, same installers as lisa-setup-remote-env; what differs is consent and that the pin is a floor rather than an equality. Run it on a fresh checkout, after a manifest change, or when a tool fails at the moment of use.
tools
Route one unit of work to a remote execution surface. Reads the executionEnv parameter (local by default, codex-cloud or claude-web today), verifies the environment is provisioned and bound to this repository, submits a thin skill invocation, records the task identifier to .lisa/remote-dispatch.json, and exits without polling. Routing only — the remote runs the identical skill from the identical repository. Composable and inline: other skills invoke it via the Skill tool rather than users calling it directly.