
React 19 Component Scaffolder
- 103 installs
- 191 repo stars
- Updated July 24, 2026
- pproenca/dot-skills
react-19-component-scaffolder is a Claude Code skill in the Frontend Development category.
Key points
- react-19-component-scaffolder
- Frontend Development
- AI-coding skill
React 19 Component Scaffolder by the numbers
- 103 all-time installs (skills.sh)
- +9 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #1,049 of 2,245 Frontend Development skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/pproenca/dot-skills --skill react-19-component-scaffolderAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 103 |
|---|---|
| repo stars | ★ 191 |
| Last updated | July 24, 2026 |
| Repository | pproenca/dot-skills ↗ |
How do I helps with frontend development tasks during AI-assisted development.?
Helps with frontend development tasks during AI-assisted development.
Who is it for?
Best when you're working on frontend development and need structured help with react 19 component scaffolder.
Skip if: Teams with no frontend development needs, or anyone wanting a generic chat assistant without this specific workflow.
When should I use this skill?
When you need to helps with frontend development tasks during AI-assisted development., or when react-19-component-scaffolder is a claude code skill in the frontend development category.
What you get
Structured output aligned to react-19-component-scaffolder: react-19-component-scaffolder, Frontend Development.
Files
React 19 Component Scaffolder
Generate React 19/19.2 components, pages, hooks, and supporting files from parameterized templates. Every template enforces the patterns codified in the sibling react skill — refs as regular props (never forwardRef), Context rendered directly as provider (never .Provider), form actions with useActionState + useFormStatus, inline document metadata, useSyncExternalStore for external subscriptions, and YMNNAE-compliant hook bodies.
When to Apply
- Creating a new React component (Server, Client, or unspecified — defaults to Server)
- Adding a new route/page in a React 19 app
- Building a form that needs progressive enhancement, optimistic UI, or pending state
- Setting up a new Context provider with state + dispatch split
- Writing a custom hook that subscribes to external state or wraps async work
- Modeling complex state with a typed reducer
- Adding document metadata (
<title>,<meta>,<link>) or resource hints (preload,preconnect,prefetchDNS) - The user says "scaffold", "boilerplate", "generate", "new component", "new page", "new hook", or names any of the template types
Available Templates
All filenames are kebab-case; the exported React identifier ({Name}, use{Name}) is PascalCase / camelCase.
| # | Template | When to use | Files generated |
|---|---|---|---|
| 1 | function-component | Generic reusable component (works in Server or Client) | {name-kebab}.tsx, {name-kebab}.test.tsx |
| 2 | server-component-page | Route page with server data fetch + Suspense + metadata | page.tsx |
| 3 | client-island | 'use client' interactivity nested inside a server page | {name-kebab}-island.tsx, {name-kebab}-island.test.tsx |
| 4 | form-action | Mutation form with useActionState + Zod schema + server action | {name-kebab}-form.tsx, actions.ts, schema.ts |
| 5 | context-provider | Shared state with state/dispatch split and accessor hook | {name-kebab}-context.tsx |
| 6 | custom-hook | YMNNAE-compliant hook (external subscription or composed effect) | use-{name-kebab}.ts, use-{name-kebab}.test.ts |
| 7 | reducer | Typed reducer with discriminated-union actions + exhaustive switch | {name-kebab}-reducer.ts, {name-kebab}-reducer.test.ts |
| 8 | head-and-hints | Document metadata + resource hints for above-the-fold assets | {name-kebab}-head.tsx |
How to Use
1. Identify the template that matches the user's request. If ambiguous, ask. Default to function-component for unqualified "make me a component" requests. 2. Read the template at assets/templates/{name}.template. 3. Read the related conventions at references/conventions.md — every template emits code that obeys these conventions. 4. Collect parameters (see "Parameters" below). Ask the user only for parameters you cannot infer from context. 5. Substitute placeholders in the template. Every placeholder is wrapped in {curly_braces}. Empty placeholders (e.g., {additional_hooks} when there are none) collapse to empty string. 6. Write each output file using the Write tool. Use the path conventions in config.json (module_path, route_path). 7. Mention the conventions enforced so the user understands why the generated code looks the way it does (e.g., "I used ref as a prop rather than forwardRef because…").
Parameters
Common parameters (most templates accept these):
| Parameter | Required | Default | Description |
|---|---|---|---|
name | yes | — | Component / hook / reducer identifier in PascalCase (or camelCase for hooks/reducers). The scaffolder derives file_name (kebab-case) and name_kebab automatically. |
module_path | no | src/components (or from config.json) | Directory under the project root for the generated files |
props | no | [] | List of {name, type, required} — emitted as the props interface |
with_test | no | true | Generate the matching *.test.tsx / *.test.ts file |
with_ref | no | false | Add ref?: Ref<T> to props (templates 1 and 3) |
Template-specific parameters are documented in each template's leading comments.
Setup
On first use, populate config.json with project-specific paths so generated files land in the right place. The defaults assume a standard src/-rooted project. To override interactively:
module_path— Where components live (e.g.,src/components,app/_components)route_path— Where Server Component pages live (e.g.,app/,src/routes/)hooks_path— Where custom hooks live (e.g.,src/hooks)test_runner—vitest(default) orjest(changes the imports in test templates)
Conforming Existing Code (Multi-File Refactor)
When the user asks to conform, modernize, or align existing components with this skill's conventions across one or more files — not to generate new code — follow `references/_conform-algorithm.md` instead of going file-by-file.
Two non-negotiables from that doc:
1. Judgment over grep. Every convention is keyed off a syntactic marker (forwardRef, <Context.Provider>, react-helmet, onSubmit=, react-dom/test-utils). Grep finds the easy cases and misses the disguised ones — a manually drilled callback ref because the author dodged forwardRef, a bespoke useState({ pending, error }) that's useActionState without the name, a document.title hand-roll. Use grep for inventory and post-hoc completeness only, never as the primary detector. 2. Convention-major, not file-major. Load all target files first, then sweep one convention at a time across all files in priority order (refs/context/forms first, naming/imports last). Reports group by convention, surfacing cross-file clusters.
For brand-new scaffolds, skip this — go straight to a template.
React 19 Patterns This Skill Refuses to Generate
forwardRef(...)wrappers — emitsfunction Name({ ref, ...props })instead<Context.Provider value={...}>— emits<Context value={...}>insteaduseFormState— emitsuseActionStateinsteadonSubmithandlers for mutations — emits<form action={serverAction}>insteaduseRef<T>()without an argument — emitsuseRef<T>(null)useEffectfor derived state, parent notification, or POST-on-state-change — refuses entirely (see conventions: State derivation)react-dom/test-utils— emitsimport { act } from '@testing-library/react'instead- Manual
<link rel="preload">JSX — emitspreload()fromreact-dominstead react-helmet/react-helmet-async— emits inline<title>/<meta>/<link>instead
Gotchas
See gotchas.md. Empty on first release — gotchas accumulate as the skill is used.
Related Skills
- `react` — Authoritative React 19 best-practices distillation (44 rules). The templates in this skill emit code that conforms to those rules.
React 19 Component Scaffolder
This curated skill mirrors SKILL.md. When maintaining it, keep templates aligned with React 19 patterns and avoid reintroducing deprecated React 18 APIs.
// {file_name}.test.tsx — Tests for {Name} (client island)
// Generated by react-19-component-scaffolder
//
// Placeholder rules:
// - {required_prop_examples}: JSX attributes for required props in render(...).
// Example: 'initialCount={0}'.
// - {default_assertion}: an assertion that exercises `screen` so the import is used.
// Example: 'expect(screen.getByRole("button")).toBeInTheDocument()'.
// - {additional_test_cases}: extra `it(...)` blocks, e.g. user-interaction tests with
// `@testing-library/user-event`. Leave empty if none.
import { describe, it, expect } from 'vitest'
import { render, screen } from '@testing-library/react'
import { {Name} } from './{file_name}'
describe('{Name}', () => {
it('renders without crashing', () => {
render(<{Name} {required_prop_examples} />)
{default_assertion}
})
{additional_test_cases}})
// {file_name}.tsx — Client island for {Name}
// Generated by react-19-component-scaffolder
//
// 'use client' marks the file as a client boundary. Receives ONLY serializable
// props from server components: strings, numbers, booleans, Date, Map, Set,
// BigInt, typed arrays, Promise, JSX elements, and Server Actions.
// Plain functions and class instances cannot cross the boundary.
//
// Placeholder rules (see function-component.tsx.template for full reference):
// - {additional_react_hooks}: ", useTransition, useRef" etc. when more hooks are
// needed alongside useState. Leave empty for just useState. Drop the entire
// useState import if state isn't needed and replace this line accordingly.
// - {additional_type_imports}: project type imports, one per line ending in \n.
'use client'
import { useState{additional_react_hooks} } from 'react'
{additional_type_imports}
export interface {Name}Props {
{prop_definitions}{with_ref_prop}}
export function {Name}({ {prop_destructure} }: {Name}Props) {
{initial_state}
return (
{jsx_body}
)
}
// {file_name}.tsx — {Name} context
// Generated by react-19-component-scaffolder
//
// React 19: render <Context> directly as provider — <Context.Provider> is legacy.
// Split state vs. dispatch into separate contexts to avoid re-rendering consumers
// that only need one of them.
{use_client_directive}
import { createContext, use{additional_hooks} } from 'react'
import type { ReactNode{dispatch_type_import} } from 'react'
{reducer_import}
{state_type_definition}
const {Name}StateContext = createContext<{state_type} | null>(null)
{dispatch_context}
export interface {Name}ProviderProps {
children: ReactNode
{provider_prop_definitions}}
export function {Name}Provider({ children{provider_prop_destructure} }: {Name}ProviderProps) {
{state_initialization}
return (
<{Name}StateContext value={state}>
{dispatch_provider_open} {children}
{dispatch_provider_close} </{Name}StateContext>
)
}
export function use{Name}() {
const value = use({Name}StateContext)
if (value === null) {
throw new Error('use{Name} must be used inside a <{Name}Provider>')
}
return value
}
{dispatch_hook}
// use-{name_kebab}.test.ts — Tests for use{Name}
// Generated by react-19-component-scaffolder
//
// Placeholder rules:
// - {act_import}: ", act" when at least one test case calls `act()`. Empty otherwise.
// - {initial_args}: arguments for the first renderHook call. Example: '()' or '("default")'.
// - {initial_assertion}: an assertion against `result.current` so the import is used.
// Example: 'expect(result.current).toBe(true)'.
// - {additional_test_cases}: extra `it(...)` blocks; include `act(...)` here when
// you need to dispatch updates.
import { describe, it, expect } from 'vitest'
import { renderHook{act_import} } from '@testing-library/react'
import { use{Name} } from './use-{name_kebab}'
describe('use{Name}', () => {
it('returns the expected initial value', () => {
const { result } = renderHook(() => use{Name}{initial_args})
{initial_assertion}
})
{additional_test_cases}})
// use-{name_kebab}.ts — Custom hook: {description}
// Generated by react-19-component-scaffolder
//
// This hook follows YMNNAE (You Might Not Need an Effect):
// - Derived state is computed during render, NOT stored with useState + useEffect
// - External subscriptions use useSyncExternalStore for tearing-free reads
// - Side effects with cleanup go in useEffect; everything else does not
{use_client_directive}
import { {react_imports} } from 'react'
{additional_imports}
{type_definitions}
export function use{Name}({hook_params_signature}): {return_type} {
{hook_body}
}
// {file_name}/actions.ts — Server actions for {Name}Form
// Generated by react-19-component-scaffolder
'use server'
import { {schema_name}, type {schema_input_type} } from './schema'
{db_imports}{revalidate_imports}
export type {Name}FormState =
| { status: 'idle'; errors: Record<string, never> }
| { status: 'error'; errors: { _form?: string } & Partial<Record<keyof {schema_input_type}, string>> }
| { status: 'success'; data: {success_data_type} }
export async function {action_name}(
_previousState: {Name}FormState,
formData: FormData,
): Promise<{Name}FormState> {
// Parse FormData into a typed object
const raw = {
{form_data_extraction} }
// Validate — server is the source of truth, never trust the client
const parsed = {schema_name}.safeParse(raw)
if (!parsed.success) {
return {
status: 'error',
errors: parsed.error.flatten().fieldErrors as {Name}FormState['errors'],
}
}
// Execute the mutation. {mutation_body} MUST return a {Name}FormState — usually
// { status: 'success', data: ... } on completion, or { status: 'error', errors: ... }
// for domain errors that aren't validation errors.
try {
{mutation_body}
} catch (error) {
return {
status: 'error',
errors: { _form: error instanceof Error ? error.message : 'Unknown error' },
}
}
}
// {file_name}/schema.ts — Validation schema for {Name}Form
// Generated by react-19-component-scaffolder
//
// Shared by the server action AND any optional client-side validation.
// Keep this module FREE of `'use server'` and `'use client'` directives.
import { z } from 'zod'
export const {schema_name} = z.object({
{schema_fields}})
export type {schema_input_type} = z.infer<typeof {schema_name}>
// {file_name}.tsx — {Name} form using React 19 form actions
// Generated by react-19-component-scaffolder
//
// Pattern: <form action={serverAction}> + useActionState + useFormStatus.
// Works without JavaScript (progressive enhancement) and shows pending state.
'use client'
import { useActionState{optimistic_import} } from 'react'
import type { ReactNode } from 'react'
import { useFormStatus } from 'react-dom'
import { {action_name} } from './actions'
import type { {Name}FormState } from './actions'
const initialState: {Name}FormState = {
status: 'idle',
errors: {},
{initial_state_extras}}
export function {Name}Form({initial_props_signature}) {
{optimistic_hook}
// Pending state is read inside <SubmitButton> via useFormStatus.
// If you also need it at the form level, destructure the third element of useActionState here.
const [state, formAction] = useActionState({action_name}, initialState)
return (
<form action={formAction} className="{form_class}">
{form_fields}
{state.status === 'error' && state.errors._form && (
<p role="alert" className="form-error">{state.errors._form}</p>
)}
<SubmitButton>{submit_label}</SubmitButton>
</form>
)
}
function SubmitButton({ children }: { children: ReactNode }) {
// useFormStatus reads parent <form> pending state — must be a child component
const { pending } = useFormStatus()
return (
<button type="submit" disabled={pending} aria-busy={pending}>
{pending ? '{pending_label}' : children}
</button>
)
}
// {file_name}.test.tsx — Tests for {Name}
// Generated by react-19-component-scaffolder
//
// Placeholder rules:
// - {required_prop_examples}: JSX attributes for required props in render(...).
// Example: 'user={{ id: "1", name: "Ada" }}'.
// - {default_assertion}: an assertion that exercises `screen` so the import is used.
// Example: 'expect(screen.getByText(/Ada/)).toBeInTheDocument()'.
// - {additional_test_cases}: extra `it(...)` blocks. Leave empty if none.
import { describe, it, expect } from 'vitest'
import { render, screen } from '@testing-library/react'
import { {Name} } from './{file_name}'
describe('{Name}', () => {
it('renders without crashing', () => {
render(<{Name} {required_prop_examples} />)
{default_assertion}
})
{additional_test_cases}})
// {file_name}.tsx — {description}
// Generated by react-19-component-scaffolder
//
// Placeholder rules:
// - {react_type_imports}: comma-separated React type imports needed by props.
// Examples: "Ref", "ReactNode", "Ref, ReactNode". Leave empty if none.
// - {type_imports_block}: the assembled `import type { ... } from 'react'` line built
// from {react_type_imports}, ending in \n. Expands to "" when no React types are needed.
// - {additional_type_imports}: import lines for project types referenced in props.
// Each line ends with \n. Example: "import type { User } from '@/types'\n".
// Leave empty if no custom types.
// - {prop_definitions}: one prop per line, 2-space indent, no trailing comma.
// Example: " user: User\n variant?: 'compact' | 'full'\n".
// - {with_ref_prop}: when with_ref=true, expands to " ref?: Ref<HTMLDivElement>\n".
// When false, expands to "".
// - {with_children_prop}: when accepting children, " children: ReactNode\n", else "".
// - {prop_destructure}: comma-separated prop names. Example: "user, variant".
// - {jsx_body}: the component body JSX expression. Single root element.
{type_imports_block}{additional_type_imports}
export interface {Name}Props {
{prop_definitions}{with_ref_prop}{with_children_prop}}
export function {Name}({ {prop_destructure} }: {Name}Props) {
return (
{jsx_body}
)
}
// {file_name} — Document metadata + resource hints for {Name}
// Generated by react-19-component-scaffolder
//
// Tags hoist to <head> automatically (React 19). Resource hints from react-dom
// are deduplicated by URL and safe to call in render.
import { preload, preconnect, prefetchDNS{preinit_import} } from 'react-dom'
export interface {Name}HeadProps {
{head_prop_definitions}}
export function {Name}Head({ {head_prop_destructure} }: {Name}HeadProps) {
{connect_calls}{preload_calls}{preinit_calls}
return (
<>
<title>{title_expression}</title>
<meta name="description" content={description_expression} />
{open_graph_block}{stylesheet_block} </>
)
}
// {name_kebab}-reducer.test.ts — Tests for {name_camel}Reducer
// Generated by react-19-component-scaffolder
//
// Placeholder rules:
// - {action_test_cases}: one `it(...)` block per action variant. Each should
// call {name_camel}Reducer(prev, { type: 'X', ...payload }) and assert the
// shape of the returned state. Reference {Name}State and {Name}Action for typing.
import { describe, it, expect } from 'vitest'
import {
{name_camel}Reducer,
initial{Name}State,
type {Name}State,
type {Name}Action,
} from './{name_kebab}-reducer'
describe('{name_camel}Reducer', () => {
it('has the expected initial state shape', () => {
const initial: {Name}State = initial{Name}State
expect(initial).toMatchSnapshot()
})
it('throws on unknown action types (assertNever exhaustive guard)', () => {
const unknownAction = { type: '__unknown__' } as unknown as {Name}Action
expect(() => {name_camel}Reducer(initial{Name}State, unknownAction)).toThrow()
})
{action_test_cases}})
// {name_kebab}-reducer.ts — Typed reducer for {Name} state
// Generated by react-19-component-scaffolder
//
// Action types use a discriminated union so the switch is exhaustive — adding
// a new action without handling it becomes a type error.
export interface {Name}State {
{state_fields}}
export type {Name}Action =
{action_union}
export const initial{Name}State: {Name}State = {
{initial_state_fields}}
export function {name_camel}Reducer(state: {Name}State, action: {Name}Action): {Name}State {
switch (action.type) {
{case_branches} default:
// Exhaustive check — if a new action.type is added, TypeScript flags this line
return assertNever(action)
}
}
function assertNever(value: never): never {
throw new Error(`Unhandled action: ${JSON.stringify(value)}`)
}
// {route_path}/page.tsx — Server Component
// Generated by react-19-component-scaffolder
//
// This file is a Server Component by default. Do NOT add 'use client'.
// Client interactivity goes in a sibling client island (see client-island template).
//
// Suspense pattern: the page does NOT await the data fetch. It creates the promise
// and passes it to {Name}Content, which calls use(promise) — so Suspense fires
// while the promise resolves. Awaiting at the page level would defeat Suspense.
//
// Placeholder rules:
// - {route_params}: typed entries in the params Promise. Example: 'slug: string'.
// - {resource_hints}: preload/preconnect/prefetchDNS calls. Each as its own line.
// Leave empty if no resource hints needed.
// - {data_fetch_block}: lines that create unawaited promises for downstream content.
// Example: 'const productPromise = db.product.findUnique({ where: { slug } })\n'.
// Leave empty for static pages with no server data.
// - {content_props} / {content_props_destructure} / {content_props_types}: how the
// promise(s) are passed to {Name}Content. Example: 'productPromise={productPromise}',
// 'productPromise', 'productPromise: Promise<Product>'.
import { Suspense, use } from 'react'
import { preload, preconnect, prefetchDNS } from 'react-dom'
{additional_imports}
interface PageProps {
params: Promise<{ {route_params} }>
searchParams: Promise<Record<string, string | string[] | undefined>>
}
export default async function {Name}Page({ params }: PageProps) {
const resolvedParams = await params
{resource_hints}
{data_fetch_block}
return (
<>
{/* Document metadata hoists to <head> automatically (React 19) */}
<title>{title_expression}</title>
<meta name="description" content={description_expression} />
{open_graph_tags}
<main>
<Suspense fallback={<{Name}Skeleton />}>
<{Name}Content {content_props} />
</Suspense>
</main>
</>
)
}
function {Name}Content({ {content_props_destructure} }: { {content_props_types} }) {
// use(promise) suspends until resolved — the parent <Suspense> shows the fallback.
{content_body}
}
function {Name}Skeleton() {
return <div aria-busy="true">Loading {name_lower}…</div>
}
{
"module_path": "src/components",
"route_path": "app",
"hooks_path": "src/hooks",
"reducers_path": "src/state",
"test_runner": "vitest",
"import_alias": "@",
"_setup_instructions": {
"module_path": "Directory under the project root where components are generated. Common values: src/components, app/_components, src/ui.",
"route_path": "Directory where Server Component pages are generated. Common values: app (Next.js App Router), src/routes, app/(routes).",
"hooks_path": "Directory for custom hooks. Common values: src/hooks, src/lib/hooks, app/_hooks.",
"reducers_path": "Directory for reducer modules. Common values: src/state, src/store, src/reducers.",
"test_runner": "Test runner used in generated test files. Supported: vitest (default) or jest. Changes the imports in test templates.",
"import_alias": "TypeScript path-alias root for non-relative imports. Common values: @ (for @/lib/db) or src (for src/lib/db). Leave empty to use only relative imports."
}
}
Gotchas
No known gotchas yet. Add entries here when scaffolding edge cases bite — be specific about what failed and what unblocked it.
Template
### {Short title — the symptom or the surprise}
{1-3 sentences describing what went wrong.}
Fix: {What unblocked it.}
Added: {YYYY-MM-DD}{
"version": "1.0.7",
"organization": "React Best Practices",
"technology": "React 19",
"discipline": "extraction",
"type": "scaffolding",
"date": "May 2026",
"abstract": "Parameterized templates for scaffolding modern React 19 / 19.2 code in TypeScript: function components with ref-as-prop, Server Component pages with inline metadata and resource hints, client islands, form-action forms with useActionState + Zod schema, context providers with the React 19 <Context value={...}> syntax, YMNNAE-compliant custom hooks, and typed reducers with exhaustive discriminated unions. Refuses to generate deprecated React 18 patterns (forwardRef, <Context.Provider>, useFormState, react-dom/test-utils, react-helmet). Pairs with the react distillation skill.",
"references": [
"https://react.dev",
"https://react.dev/blog/2024/04/25/react-19-upgrade-guide",
"https://react.dev/blog/2024/12/05/react-19",
"https://react.dev/blog/2025/10/01/react-19-2",
"https://react.dev/learn/you-might-not-need-an-effect"
]
}
Conform Algorithm — Bringing Existing Code In Line With These Conventions
Use this when the user asks to conform, modernize, or align existing components with this skill's conventions across one or more files (e.g. "make these match our scaffolded shape", "modernize this folder to the React 19 conventions", "audit this PR against our scaffolding rules").
For brand-new code, ignore this doc — go directly to a template. This algorithm exists for the retrofit case, where templates can't be used directly.
---
Principle 1 — Judgment over grep
Every convention in `conventions.md` is anchored to a syntactic marker (forwardRef, <Context.Provider>, react-helmet, e.preventDefault(), react-dom/test-utils). Those markers are easy to grep for and easy to miss the point of.
The decision rule for every convention is: "Does this code break the convention, in spirit?" — not "Does this string appear?"
| What grep finds | What grep misses (the high-value conforms) |
|---|---|
forwardRef( in an import | A component drilling a setRef callback through 3 props because the author was avoiding forwardRef awkwardness |
<Context.Provider> JSX | A bespoke useState + module-level Set pub/sub re-implementing context |
onSubmit= JSX attribute | A form using <button onClick> + manual useState({ pending, error }) — the exact useActionState shape without the name |
react-helmet import | A useEffect(() => { document.title = ... }, [...]) hand-roll |
react-dom/test-utils import | A test calling flushSync + unstable_* to coerce render order |
useRef<T>() | useRef<T>(undefined as any) casting tricks |
Use grep/AST only to:
- Take inventory at the start (count files, list components, list templates that would apply)
- As a post-hoc completeness check after judgment-based review (e.g. confirm zero remaining
forwardRefafter refactor)
Never use grep as the primary detector for a convention violation. The judgment-based reads catch the disguised cases.
---
Principle 2 — Convention-major sweep, not file-major
For N files against 12 conventions, the natural reflex is file-major (walk each file, check all 12 conventions). It fails the same way it fails for the react skill: late files and low-priority conventions get silently skipped, and cross-file clusters stay invisible.
Do this instead — convention-major:
1. Load all target files into context up front (read them all before starting).
2. For each convention in conventions.md, in priority order (see below):
a. State the convention's pattern in one sentence (the intent, not the marker).
b. Sweep every applicable file simultaneously, looking for breaks of that pattern.
c. Record findings grouped by convention, with file:line references.
3. After all conventions are swept, present findings ordered by convention × severity.Convention priority order
conventions.md lists conventions topically. For a sweep, walk them in this priority:
1. Refs as regular prop, not `forwardRef` — HIGH (deprecated path; codemod available) 2. Context: `<Context value={...}>`, not `.Provider` — HIGH (deprecated path) 3. Forms: server action, not `onSubmit` for mutations — HIGH (progressive enhancement; pending state correctness) 4. State derivation: render-time, not effects — HIGH (correctness — sync holes; covers a large surface) 5. `useRef<T>(null)` always pass initial value — MEDIUM (TS breaking change) 6. External subscriptions: `useSyncExternalStore`, not manual `useEffect` + listeners — MEDIUM (concurrent-safe) 7. Test imports: `act` from `react`/`@testing-library/react`, never `react-dom/test-utils` — MEDIUM (removed in 19) 8. Server-side validation, always — MEDIUM (security) 9. Server Components by default, Client by exception — MEDIUM (bundle size) 10. Exhaustive switches in reducers — LOW (catches a class of bugs at compile time) 11. Custom hook naming: `use{Verb}{Noun}` / `use{Noun}` — LOW (clarity) 12. File naming + import grouping — LOW (cosmetic, formatter-equivalent)
---
Procedure
Step 0 — Confirm the file set
Get the explicit list of files from the user. Don't sweep an unbounded directory.
Step 1 — Inventory pass
Read every file once. Tag each as:
- Component (Server or Client — note which)
- Custom hook module
- Reducer module
- Context provider
- Form / action module
- Test file
- Other
Use the tag set to filter applicable conventions per file (e.g. "Server-side validation" applies only to action modules; "Exhaustive switches in reducers" applies only to reducer modules).
Step 2 — Convention-major sweep
For each convention in the priority order above, do one pass over all applicable files. For each file, ask: "Does this code achieve the convention's intent, in spirit?"
When you find a break, record:
- File:line — exact location
- Pattern break — one sentence in the spirit of the convention ("this form uses
e.preventDefault()+ manualuseState({ pending, error })— the exactuseActionStateshape, without the name") - Conform action — the concrete shape change needed (point to a template if one applies: e.g. "shape matches
form-action.tsx.template— replace with that shape")
Step 3 — Report findings
Group by convention. Within a convention, group by file. Add a Cross-file observations subsection per convention when 2+ files share the same break.
Step 4 — Apply (optional)
When the user approves:
- Apply by convention, not by file — finish all of convention 1 across all files before starting convention 2.
- For mechanically-replaceable cases (
<Context.Provider>→<Context>,forwardRefremoval), prefer the official codemods and note them. Don't reinvent. - After applying a convention, take an inventory pass to confirm no regressions.
---
What this algorithm refuses to do
- Sweep an unbounded directory — demand a scope.
- File-major reports — never emit findings as "## src/Foo.tsx — issues: …" headings. Always group by convention.
- Grep-only findings — read the surrounding 30 lines before declaring a break. Grep is the trigger, never the verdict.
- Cosmetic-only sweeps that look like refactors —
<Context.Provider>→<Context>is a codemod. The skill-worthy conforms are the shape changes (form refactors touseActionState, derived state moved to render, external subscriptions touseSyncExternalStore).
---
Quick sanity check before reporting
- Did I sweep every applicable convention against every applicable file?
- For each finding, is the evidence holistic (I read the surrounding code) or just a keyword match?
- Did I surface cross-file clusters where they exist?
- Did I propose a template (or codemod) where one fits, instead of hand-writing the shape?
- If a convention had zero findings, did I say so explicitly?
React 19 Scaffolding Conventions
These are the conventions every template enforces. Each one is here because not following it has caused real problems — the rationale matters more than the rule.
Placeholder substitution rules
Templates use {placeholder_name} syntax. The full placeholder reference for each template lives in its header comment block — read it before substituting. General rules:
- Single token (
{Name},{name_kebab}): expands to a single identifier with no surrounding whitespace. - Block content (
{prop_definitions},{action_test_cases},{content_body}): expands to one or more complete lines, each with its own trailing\n. Indent inside the block matches the position of the placeholder. - Comma-prefixed lists (
{additional_react_hooks},{optimistic_import}): expand to, item1, item2when present, empty string when absent. Templates expect the leading comma. - Optional lines (
{with_ref_prop},{with_children_prop}): expand to a complete line including its\n, or empty string. Two consecutive optional lines safely collapse to nothing. - Optional imports (
{additional_type_imports},{reducer_import}): expand to zero or more fullimport ...lines, each ending in\n, or empty string.
When a placeholder is absent, substitute the empty string — never leave the literal {...} in the output.
---
File naming: kebab-case files, PascalCase components
Files use kebab-case (user-card.tsx); the exported React component uses PascalCase (UserCard).
Why: macOS is case-insensitive by default; Linux CI is case-sensitive. Mixed-case filenames cause "works on my machine, fails in CI" bugs. The kebab-case file / PascalCase component split keeps the component identity readable in JSX while staying portable on disk.
Exception: Next.js route files (page.tsx, layout.tsx, loading.tsx, error.tsx) keep their framework-mandated names.
Refs: regular prop, never forwardRef
Templates emit:
interface InputProps {
ref?: Ref<HTMLInputElement>
placeholder?: string
}
function Input({ ref, placeholder }: InputProps) { /* ... */ }Not:
const Input = forwardRef<HTMLInputElement, InputProps>(/* ... */)Why: React 19 made ref a regular prop on function components. forwardRef will be deprecated in a future major. New code using forwardRef is born deprecated. The codemod path (npx codemod@latest react/19/replace-forwardRef) goes one direction — we generate code that doesn't need it.
Context: <Context value={...}>, never .Provider
Templates emit:
<UserContext value={user}>{children}</UserContext>Not:
<UserContext.Provider value={user}>{children}</UserContext.Provider>Why: Same as refs — .Provider is the legacy syntax. React 19 rewrote the React developer surface to make Context directly renderable.
useRef<T>(null) — always pass an initial value
Templates emit:
const inputRef = useRef<HTMLInputElement>(null)Not:
const inputRef = useRef<HTMLInputElement>()Why: React 19 made the argument required (TypeScript breaking change). The single-argument form is also clearer — readers see immediately what the ref starts as.
Forms: server action, never onSubmit for mutations
Templates emit:
<form action={createUser}>Not:
<form onSubmit={(e) => { e.preventDefault(); createUser(/* ... */) }}>Why: Form actions work without JavaScript (progressive enhancement). They eliminate preventDefault boilerplate. They integrate with useActionState, useFormStatus, and useOptimistic for pending and optimistic UI. Reaching for onSubmit regresses to React 18.
Exception: Client-only forms with no server (e.g., a search box driving a router push). Even there, <form action={fn}> with a client-side function is the cleaner pattern.
Server Components by default, Client Components by exception
Templates default to no 'use client'. The client island template (client-island.tsx.template) is the only one that emits the directive, and only because that's its purpose.
Why: 'use client' boundaries become bundle inclusions. The lower in the tree the boundary lives, the less JavaScript ships. The default of "Server Component unless we explicitly need state, event handlers, or browser APIs" produces smaller bundles.
Test imports: act from react, never react-dom/test-utils
Test templates emit:
import { act } from '@testing-library/react'
// or, when needed standalone:
import { act } from 'react'Why: react-dom/test-utils was removed in React 19. The codemod (npx codemod@latest react/19/replace-act-import) moves to react. New test files should never grow that import.
Custom hook naming: use{Verb}{Noun} or use{Noun}
Templates emit useToggleTheme, useDebouncedValue, useUserPreferences — verbs when the hook drives a behavior, plain noun when it exposes state.
Why: Disambiguates side-effecting hooks from pure-read hooks. useTheme() reads; useToggleTheme() toggles. Mixing the two in the same code becomes confusing.
State derivation: render-time, not effects
The custom-hook and reducer templates have NO useEffect calls for derived state. If the value can be computed from props/state, it is — const fullName = ${first} ${last}``.
Why: Effects for derived state cause extra render passes and create sync holes (state can be stale between updates). You Might Not Need an Effect catalogues a dozen variants of this anti-pattern. We refuse to generate any of them.
External subscriptions: useSyncExternalStore, not manual useEffect + listeners
The custom-hook template offers a subscribe_target parameter; when set, it generates a useSyncExternalStore skeleton, not an effect-based subscription.
Why: useSyncExternalStore is tearing-free in concurrent rendering, SSR-safe via the third argument, and has the cleanup wired in for you. Manual useEffect + addEventListener is a pre-React-18 pattern.
Exhaustive switches in reducers
The reducer template ends every switch with:
default:
return assertNever(action)Why: Adding a new action variant without handling it becomes a TypeScript error rather than a silent runtime fall-through. The cost is one helper function; the value is catching unhandled actions at compile time.
Server-side validation, always
The form-action template's actions.ts always calls schema.safeParse(raw) on the server. Client-side validation is optional cosmetic; server-side validation is non-negotiable.
Why: Client-side validation can be bypassed (disable JS, edit the DOM, hit the action directly). The server is the only place where validation is enforceable. Skipping server validation is a security bug, not a style choice.
Imports: framework → external → internal → relative, blank line between groups
Templates lay out imports in four blocks separated by blank lines:
import { useState } from 'react' // framework
import { useFormStatus } from 'react-dom'
import { z } from 'zod' // external
import { db } from '@/lib/db' // internal alias
import { schema } from './schema' // relativeWhy: Auto-formatters (Biome, Prettier with plugins, ESLint import/order) all agree on this grouping. Keeping the convention prevents merge churn from formatter disagreements. The visual grouping also makes "is this a third-party import?" obvious at a glance.
Related skills
FAQ
What does react-19-component-scaffolder do?
react-19-component-scaffolder is a Claude Code skill in the Frontend Development category.
When should I use react-19-component-scaffolder?
When you need to helps with frontend development tasks during AI-assisted development., or when react-19-component-scaffolder is a claude code skill in the frontend development category.
What are the main capabilities?
react-19-component-scaffolder; Frontend Development; AI-coding skill.