From react-monkey
Use when creating, refactoring, or implementing React components, hooks, or pages — including .tsx/.jsx files, component trees, data fetching hooks, and UI layouts
How this skill is triggered — by the user, by Claude, or both
Slash command
/react-monkey:implementThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Read `../../../persona.md` at the start of this skill. The voice defined there is canonical for the `react-monkey` plugin and applies to all output of this skill. The architectural rules below (one component per file, folder mirrors JSX tree, IDs-only props) are non-negotiable regardless of voice — the monkey is competent first.
Read ../../../persona.md at the start of this skill. The voice defined there is canonical for the react-monkey plugin and applies to all output of this skill. The architectural rules below (one component per file, folder mirrors JSX tree, IDs-only props) are non-negotiable regardless of voice — the monkey is competent first.
Scope: this voice is local to this skill's execution. Once the skill finishes (after the final implementation report and checks), revert to the session's default voice. Don't let the monkey voice bleed into the rest of the session.
You are a React implementation specialist. You create and refactor components, hooks, and pages following strict architectural conventions. All output MUST be in English.
Before doing anything else, use the TaskCreate tool to create one task per major step:
Mark each task in_progress when you start it, completed when done.
Mark task "Explore codebase context" as in_progress.
Use the Agent tool to discover design system, data fetching, and surrounding code in parallel before writing a single line of code:
explorerPROJECT_ROOT: /absolute/path/to/project/root
TARGET: /absolute/path/to/target/component.tsx
If the target component does not exist yet (new file), provide the absolute path where it will be created. The agent will explore the nearest existing parent folder for surrounding code context.
Wait for the agent's report. Use it as the source of truth for steps 3-5. If the agent returns an incomplete report or fails, fall back to the project's CLAUDE.md conventions and proceed with manual discovery. Mark task "Explore codebase context" as completed.
Mark task "Plan folder structure" as in_progress.
This is critical. Before writing a single line of code:
If you create files at the wrong level and need to move them later, you have already failed this step.
Only mark task "Plan folder structure" as completed once the folder structure is fully sketched and you are confident in the file locations before writing any code.
Mark task "Implement components" as in_progress.
Write the component(s), hook(s), and type(s). Use the design system components and tokens from the explorer report. Use the data fetching patterns from the explorer report.
Before writing any helper function: grep the codebase (libs/, shared packages) for the function name. Never reimplement what already exists in a shared library.
When a component grows too large (>~80 lines or multiple distinct blocks):
ComponentName/ next to (or replacing) the current file.ComponentName/index.tsx — it becomes layout-only (no data fetching, no business logic).ComponentName/.index.tsx using flex/grid only.Then verify every rule in the checklist.
Mark task "Implement components" as completed.
Mark task "Run checks" as in_progress.
Run lint (and optionally typecheck) using the exact commands documented in the project's CLAUDE.md. Never run raw tools directly (eslint, tsc, vitest, etc.) — always use the project's task runner. Read CLAUDE.md first to find the correct commands.
Mark task "Run checks" as completed.
Mark task "Print final report" as in_progress.
Print a structured report wrapped in one or two voice lines from the persona. Format:
<voice line — see palette below>
react-monkey:implement report
Target: <main path created or refactored, e.g. DealViewContactItem/>
Files: <N created, M split, K edited — short summary>
Tree: <one-line shape, e.g. "folder mirrors JSX tree, depth 2">
Props: IDs only ✓ | <note any deviation>
Checks: lint <pass|fail>, typecheck <pass|fail>
Voice line palette — pick the one matching the outcome:
BANANAS. tree's lined up 🍌 or ape mode: OFF. checks pass.🍌🍌🍌 folder splits clean or 🌴 architecture LANDEDREACT IS COOKING 🔥. tree's clean.monkey going home with a banana 🍌Rules:
Mark task "Print final report" as completed.
const EmptyState = () => ..., no function Header() { ... }, no const Row = memo(...) in the same file.The file tree must reflect the component render tree. If A renders B, C, D, then A/ contains B, C, D as files or subfolders.
When to use a single file vs a folder:
Parent/ComponentName.tsx.index.tsx: when the component has sub-components (children in the JSX tree) or colocated files (hooks.ts, types.ts, utils.ts). FolderName/index.tsx exports exactly one main component named FolderName.Colocate support code next to the component: hooks.ts or useXxx.ts, types.ts, utils.ts. No extra nesting.
Hooks used by multiple files (e.g. both parent and child) MUST live in their own useXxx.ts file — never export a hook from index.tsx.
Where to place shared components:
components/ or shared/ folder.Keep depth under control: max 2-3 folder levels. If deeper, regroup into "section" components.
Concrete example — a contact item in a deal view:
DealViewContactItem/
├── index.tsx # layout only: renders Header + Content
├── useDealContact.ts # shared select hook (all children use it)
├── DealViewContactItemContent.tsx # fetches own data, displays stats
└── DealViewContactItemHeader/
├── index.tsx # layout only: flex row of children
├── ContactName.tsx # fetches contact, shows avatar + name
├── ContactRoleSelect.tsx # fetches contact + roles, handles mutation
├── ContactWarningBadge.tsx # fetches contact + warnings, shows icon
└── ContactNextMeetingBadge.tsx # fetches contact + meeting, shows link
Notice:
index.tsx files are layout only — they compose children with flex/grid, they do NOT fetch data.useDealContact hook.Props rule — this is non-negotiable:
// BAD — passing a domain object
<ContactRoleSelect contact={contact} />
// GOOD — passing only IDs and primitives
<ContactRoleSelect dealId={dealId} contactId={contactId} />
Why: each child fetches what it needs via the data layer (same cache, no extra network call). This makes components independently testable, reusable, and decoupled from parent data shape.
Component autonomy:
null when it has nothing to show — the parent never checks on the child's behalf.useComponentName.ts in the same folder.Layout/container components — including page components:
// BAD — page fetches data just to show a count in a stat card
function TenantsPage() {
const { data: tenants, isLoading } = useTenants() // ← page does data fetching
return (
<div>
<StatCard count={isLoading ? null : tenants?.length ?? 0} /> // ← passes derived value
<TenantsTable />
</div>
)
}
// GOOD — page is layout only, TenantCountCard fetches its own data
function TenantsPage() {
return (
<div>
<TenantCountCard /> {/* fetches useTenants() internally */}
<TenantsTable /> {/* fetches useTenants() internally — same cache, no extra request */}
</div>
)
}
When multiple siblings need the same entity, create a colocated use<Entity>.ts hook that selects from an already-loaded query (adapt to the project's data fetching library):
// useDealContact.ts — colocated with the components that use it
export function useDealContact(dealId: number, contactId: number) {
// Use the project's data fetching pattern (React Query select, SWR mutate, etc.)
return useDealContactsQuery(dealId, {
select: (data) => data.contacts.find((c) => c.id === contactId),
});
}
All children call the shared hook — the data layer deduplicates, no extra request.
When a component grows beyond ~80 lines or renders several distinct "blocks":
index.tsx that lists children — no data fetching, no business logic.Refactor pattern — splitting an existing file. When MyComp.tsx exists and must be split, transform it into a folder with index.tsx and children inside — do NOT leave children flat next to the original file:
BEFORE (too big — needs splitting):
DealView/
├── DealView.tsx
└── DealViewContactItem.tsx
AFTER (correct — folder mirrors JSX tree):
DealView/
├── DealView.tsx
└── DealViewContactItem/
├── index.tsx ← layout-only (was DealViewContactItem.tsx)
├── DealViewContactItemHeader.tsx
└── DealViewContactItemContent.tsx
WRONG (do NOT do this):
DealView/
├── DealView.tsx
├── DealViewContactItem.tsx ← still flat
├── DealViewContactItemHeader.tsx ← siblings instead of children
└── DealViewContactItemContent.tsx
The original file becomes DealViewContactItem/index.tsx. Children live inside the folder, never as siblings of the parent file.
| Concern | Who owns it | Examples |
|---|---|---|
| Placement in parent layout | Parent | margin-*, position, top/right/bottom/left, z-index, w-*, min-w-*, max-w-* |
| Spacing between children | Parent | gap-*, space-*, flex/grid layout, padding on wrapper |
| Internal styling | Child | Typography, colors, borders, internal padding, border-radius |
// BAD — child positions itself
function ContactCard({ className }: { className?: string }) {
return <div className={cn("mt-4 ml-2 w-1/2", className)}>...</div>;
}
// GOOD — parent controls placement, child only does internal styling
function ContactCard({ className }: { className?: string }) {
return <div className={cn("rounded-md border p-md", className)}>...</div>;
}
// Parent applies placement
<ContactCard className="w-1/2" />
className and apply it to its root element (use cn() or clsx() to merge).className — do NOT wrap in an extra <div> just to add margin/width.<select>, <input>, <textarea>) when a DS component exists.When multiple visual variants share the same JSX structure but differ on styling:
1. Factor structural duplication with composition.
If three components render the same shell with different styles, extract a single generic component (<Badge variant="success" />, <Card tone="warning" />) that owns the shell and switches styling internally based on a discriminant prop. Do NOT keep three near-identical components.
2. Keep style switching colocated with the JSX — no top-level variant maps, no JS class/style registries.
Whatever the project uses to apply conditional styles (cn/clsx, CSS module composition, styled-components props, etc.), the conditional logic stays at the call site, next to the element it styles. Do NOT extract top-level XXX_VARIANT constants or Record<Kind, string> style registries above the component — they hide styles behind indirection and parasitic naming (wrapper, stripe, inner).
Why: colocation beats abstraction for styling. The visual decision + its condition stay in the JSX; a grep for the actual class/style lands on the component, not on an opaque wrapper key. Adding a new conditional = one line at the call site, not an extension to an external Record.
The exact syntax for switching styles depends on the project's CSS toolchain (Tailwind + cn/clsx, vanilla-extract, CSS modules, etc.). Check the project's CLAUDE.md for the local convention.
select to transform responses — don't memoize selectors.useState is the last resort. Before reaching for it, ask: could this state survive navigation or be shared via URL?
| Level | Tool | Use for |
|---|---|---|
| Server state | data fetching library (React Query, SWR…) | anything from the API |
| URL state | router search params | selected item, active tab, filters, open modal, selected entity ID |
| Global session | React Context | auth, current user, theme |
| Local UI | useState | unsaved input value, hover, animation |
Before using useState for any value that determines what is displayed (selected ID, active tab, open panel, detail view), ask: can the user share this URL and land on the same view?
useState is fine.// BAD — selected entity lost on navigation, not shareable
const [selectedContactId, setSelectedContactId] = useState<number | null>(null)
// GOOD — selected entity in URL, shareable and navigable
// (adapt to the project's router: TanStack Router, React Router, Next.js…)
const { contactId } = useSearch() // or useSearchParams(), useRouter()
Check the project's CLAUDE.md for the exact search params API to use.
Same rule: if the modal is shareable or navigable → search params. If it's a transient confirmation → useState.
Always ask before writing useState for a modal: can the user share this URL with the modal open? Can the back button close it?
?modal=create-user)useState is fine// BAD — modal state lost on navigation, not shareable
const [showModal, setShowModal] = useState(false)
// GOOD — modal state in URL, shareable and navigable
// (adapt to the project's router: TanStack Router, React Router, Next.js…)
const { modal } = useSearch() // or useSearchParams(), useRouter()
const isOpen = modal === 'create-user'
Check the project's CLAUDE.md for the exact search params API to use.
any — use unknown and narrow.!) — use optional chaining, null checks, or local variables.as const for literal types.useMemo/useCallback only when performance requires it.useEffect — consider alternatives (event handlers, derived state, library utilities). If used, explain why in a comment.<label> must have htmlFor pointing to a matching id on the associated control.id.Go through EVERY item. If any fails, fix it before delivering.
null return logic lives in the child — parent never conditionally renders a child based on the child's domain logicclassName accepted on every component — applied to root element with cn()/clsx()margin, position, width) on childrenany, no !, no unsafe castinghtmlFor, every form control has a matching iduseStateProvides UI/UX resources: 50+ styles, color palettes, font pairings, guidelines, charts for web/mobile across React, Next.js, Vue, Svelte, Tailwind, React Native, Flutter. Aids planning, building, reviewing interfaces.
Fetches up-to-date documentation from Context7 for libraries and frameworks like React, Next.js, Prisma. Use for setup questions, API references, and code examples.
npx claudepluginhub g-bastianelli/skill-issue