本文へ移動
cccskills
無料GitHub で公開

react-forms

Form patterns for React: react-hook-form (most common), Formik (legacy/stable), TanStack Form (newer). Validation via zod / yup / valibot. Controlled vs uncontrolled inputs, field arrays, multi-step wizards. Use this skill to: - Wire react-hook-form with a validation schema. - Pick between controlled and uncontrolled patterns. - Build multi-step forms with state preservation. - Handle async validation (e.g., username availability). - Integrate forms with TanStack Query mutations. Do NOT use this skill for: - General state management (see react-state-management). - Routing (see react-routing). - Component conventions (see react-conventions). - Testing forms (see react-testing).

インストール方法を見る

含まれるファイル(1)

  • SKILL.md10.8 KB

SKILL.md(原文)

インストールする前に、エージェントに与えられる指示の中身を確認できます。

React Form Patterns

Forms are where state, validation, accessibility, and UX intersect. Pick the library the project uses; don't introduce a new one without BA approval.

Detection

Marker (in dependencies)Library
react-hook-formReact Hook Form (most common, recommended for new)
formikFormik (legacy but stable)
@tanstack/react-formTanStack Form (newer, type-safe)
(none)Plain React with useState per field — fine for simple forms
zod / yup / valibot / joiValidation library — pair with the form lib via resolver

React Hook Form (recommended)

pnpm add react-hook-form. Pair with @hookform/resolvers + zod for validation.

Basic form

import { useForm, SubmitHandler } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';

const Schema = z.object({
  email: z.string().email('Invalid email'),
  password: z.string().min(8, 'At least 8 characters'),
  remember: z.boolean(),
});
type FormData = z.infer<typeof Schema>;

export function LoginForm() {
  const {
    register,
    handleSubmit,
    formState: { errors, isSubmitting },
  } = useForm<FormData>({
    resolver: zodResolver(Schema),
    defaultValues: { email: '', password: '', remember: false },
  });

  const onSubmit: SubmitHandler<FormData> = async (data) => {
    await login(data);
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <label>
        Email
        <input type="email" {...register('email')} aria-invalid={!!errors.email} />
        {errors.email && <p role="alert">{errors.email.message}</p>}
      </label>

      <label>
        Password
        <input type="password" {...register('password')} aria-invalid={!!errors.password} />
        {errors.password && <p role="alert">{errors.password.message}</p>}
      </label>

      <label>
        <input type="checkbox" {...register('remember')} />
        Remember me
      </label>

      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? 'Logging in...' : 'Log in'}
      </button>
    </form>
  );
}

Why react-hook-form

  • Uncontrolled inputs by default — minimal re-renders.
  • TypeScript inference from schema (via z.infer).
  • Accessibility-friendly: pairs naturally with aria-invalid, role="alert".
  • Performance: 1 re-render per field-touch instead of N (controlled).

Controller (for non-native inputs)

When using a third-party UI library (MUI, Mantine, AntD select, custom components):

import { Controller } from 'react-hook-form';
import { Select, MenuItem } from '@mui/material';

<Controller
  name="role"
  control={control}
  render={({ field, fieldState }) => (
    <Select {...field} error={!!fieldState.error}>
      <MenuItem value="admin">Admin</MenuItem>
      <MenuItem value="user">User</MenuItem>
    </Select>
  )}
/>

Controller adapts uncontrolled-friendly libraries to controlled APIs.

Field arrays

For dynamic lists of inputs (e.g., add multiple emails):

import { useFieldArray, useForm } from 'react-hook-form';

type FormData = { contacts: { email: string }[] };

export function ContactsForm() {
  const { control, register, handleSubmit } = useForm<FormData>({
    defaultValues: { contacts: [{ email: '' }] },
  });
  const { fields, append, remove } = useFieldArray({ control, name: 'contacts' });

  return (
    <form onSubmit={handleSubmit(...)}>
      {fields.map((field, index) => (
        <div key={field.id}>
          <input {...register(`contacts.${index}.email`)} />
          <button type="button" onClick={() => remove(index)}>Remove</button>
        </div>
      ))}
      <button type="button" onClick={() => append({ email: '' })}>Add</button>
    </form>
  );
}

field.id is a stable React key generated by RHF — DON'T use index as key.

Async validation

For async checks (e.g., username available?):

const Schema = z.object({
  username: z.string().min(3).refine(
    async (u) => {
      const res = await fetch(`/api/users/check?u=${u}`);
      return (await res.json()).available;
    },
    'Username taken'
  ),
});

const { register } = useForm({ resolver: zodResolver(Schema), mode: 'onBlur' });

mode: 'onBlur' validates after the user leaves the field — appropriate for expensive checks.

Watching values

const password = useWatch({ control, name: 'password' });
// Re-renders only this component when 'password' changes

For dependent fields (e.g., confirm password matches):

const Schema = z.object({
  password: z.string().min(8),
  confirmPassword: z.string(),
}).refine((data) => data.password === data.confirmPassword, {
  path: ['confirmPassword'],
  message: 'Passwords do not match',
});

Reset and prefill

// Prefill from server
useEffect(() => {
  reset({ email: user.email, name: user.name });
}, [user, reset]);

// Reset after submit
const onSubmit = async (data) => {
  await save(data);
  reset();
};

Multi-step forms

Two patterns:

Pattern A — single form, conditional UI:

const [step, setStep] = useState(0);
const { register, handleSubmit, trigger, formState } = useForm<FormData>({ resolver: zodResolver(Schema), mode: 'onTouched' });

const next = async () => {
  const valid = await trigger(['email', 'password']); // validate only step's fields
  if (valid) setStep(step + 1);
};

return (
  <form onSubmit={handleSubmit(onSubmit)}>
    {step === 0 && <Step1 register={register} errors={formState.errors} />}
    {step === 1 && <Step2 register={register} errors={formState.errors} />}
    {step < lastStep ? <button type="button" onClick={next}>Next</button> : <button type="submit">Submit</button>}
  </form>
);

Pattern B — separate sub-forms with shared store (Zustand): each step has its own form; persist between steps in a store. Harder but better for very long flows.

Formik (legacy)

Still common in older projects. Similar concepts:

import { Formik, Form, Field, ErrorMessage } from 'formik';
import * as Yup from 'yup';

const Schema = Yup.object({
  email: Yup.string().email().required(),
  password: Yup.string().min(8).required(),
});

<Formik
  initialValues={{ email: '', password: '' }}
  validationSchema={Schema}
  onSubmit={async (values, { setSubmitting }) => {
    await login(values);
    setSubmitting(false);
  }}
>
  {({ isSubmitting }) => (
    <Form>
      <Field type="email" name="email" />
      <ErrorMessage name="email" component="div" />
      <Field type="password" name="password" />
      <ErrorMessage name="password" component="div" />
      <button type="submit" disabled={isSubmitting}>Submit</button>
    </Form>
  )}
</Formik>;

Formik renders all fields on every keystroke (controlled inputs) — performance suffers on large forms. Modern projects prefer react-hook-form.

TanStack Form (newer)

pnpm add @tanstack/react-form. Type-safe, framework-agnostic core. Modern alternative.

import { useForm } from '@tanstack/react-form';

function MyForm() {
  const form = useForm({
    defaultValues: { email: '', password: '' },
    onSubmit: async ({ value }) => {
      await login(value);
    },
  });

  return (
    <form onSubmit={(e) => { e.preventDefault(); form.handleSubmit(); }}>
      <form.Field name="email" validators={{ onChange: ({ value }) => !value.includes('@') ? 'Invalid' : undefined }}>
        {(field) => (
          <>
            <input value={field.state.value} onChange={(e) => field.handleChange(e.target.value)} />
            {field.state.meta.errors.length > 0 && <p>{field.state.meta.errors.join(', ')}</p>}
          </>
        )}
      </form.Field>
      <button type="submit">Submit</button>
    </form>
  );
}

Plain React (no library) — when sufficient

For very simple forms (1-3 fields, no validation library, no async):

const [email, setEmail] = useState('');
const [password, setPassword] = useState('');

<form onSubmit={(e) => { e.preventDefault(); login({ email, password }); }}>
  <input type="email" value={email} onChange={(e) => setEmail(e.target.value)} required />
  <input type="password" value={password} onChange={(e) => setPassword(e.target.value)} required />
  <button>Submit</button>
</form>;

Built-in HTML5 validation (required, type="email", pattern) is free.

Integration with TanStack Query

const createUser = useCreateUser(); // useMutation from TanStack Query
const { register, handleSubmit, setError, formState } = useForm<FormData>({ resolver: zodResolver(Schema) });

const onSubmit = handleSubmit(async (data) => {
  try {
    await createUser.mutateAsync(data);
    navigate('/users');
  } catch (err) {
    if (err instanceof FetchError && err.status === 409) {
      setError('email', { message: 'Email already in use' });
    } else {
      setError('root', { message: 'Something went wrong' });
    }
  }
});

setError('root', ...) is a special key for form-level errors not tied to a single field. Show via errors.root?.message.

Accessibility checklist

  • Every input has a <label> (visible or aria-label).
  • aria-invalid on inputs with errors.
  • role="alert" on error messages (or aria-live="polite").
  • Focus moves to the first error after submit (focus() the input, or use react-hook-form's shouldFocusError).
  • Submit button shows loading state (disabled + visual indicator).

Anti-patterns

  • ❌ Adding both react-hook-form and formik — pick one.
  • ❌ Calling setState for every field in a controlled setup with 30 fields — performance dies. Use react-hook-form.
  • ❌ Skipping validation on the client AND server — always validate on both.
  • ❌ Showing errors before user has touched the field. Use mode: 'onBlur' or 'onTouched'.
  • ❌ Forgetting to disable the submit button while submitting — leads to double-submits.
  • ❌ Storing form values in a global store while the form is open — local state is fine.
  • ❌ Validating on every keystroke for expensive checks (DB lookup) — debounce or use mode: 'onBlur'.
  • ❌ Resetting the form to empty after a server error — keeps user trapped re-typing.

レビュー

まだレビューはありません。使ってみた感想をお寄せください。

同じリポジトリのスキル

概要と使いどころ

Angular 18-21 project structure, standalone components vs NgModule, control flow (@if/@for/@switch + *ngIf/*ngFor legacy), decorators, dependency injection (inject() function), lifecycle hooks, pipes, Angular Universal SSR pointer. Use this skill to: - Detect project style (standalone vs NgModule) and apply matching patterns. - Pick correct decorators and DI approach. - Use modern control flow (@if/@for/@switch) in Angular 17+ projects. - Apply `inject()` function over constructor injection where appropriate. - Wire bootstrap correctly (bootstrapApplication for standalone, AppModule for legacy). Do NOT use this skill for: - State management (see angular-state-and-rx). - Routing (see angular-routing). - Forms (see angular-forms). - Testing (see angular-testing).

日本語の概要は準備中です。原文の説明を表示しています。

AratKruglik/claude-sdlc362026年9月21日 更新

Angular forms: Reactive Forms (preferred — typed FormGroup/FormControl since Angular 14, FormBuilder, custom + async validators, FormArray, multi-step) and Template-driven (`[(ngModel)]` + FormsModule). Validation strategies, server error mapping, accessibility. Use this skill to: - Build Reactive Forms with typed FormGroup/FormControl. - Use FormBuilder для concise syntax. - Implement custom synchronous and async validators. - Wire FormArray for dynamic field lists. - Map server errors back to form fields. - Pick Reactive vs Template-driven (prefer Reactive). Do NOT use this skill for: - General conventions (see angular-conventions). - State management beyond forms (see angular-state-and-rx). - Routing (see angular-routing). - Testing forms (see angular-testing).

日本語の概要は準備中です。原文の説明を表示しています。

AratKruglik/claude-sdlc362026年9月21日 更新

Angular Router (built-in `@angular/router`) — route configuration for standalone and NgModule projects, functional guards (Angular 14.1+), lazy loading, route resolvers, typed params via signals/observables, programmatic navigation, route data and meta. Use this skill to: - Configure routes (standalone-style or NgModule-style). - Use functional guards (canActivate as function, preferred over class-based in 17+). - Lazy-load components or feature modules. - Implement auth guards via route meta + functional guards. - Read params/queries via `inject(ActivatedRoute)` + signals or RxJS. Do NOT use this skill for: - General conventions (see angular-conventions). - State management (see angular-state-and-rx). - Forms (see angular-forms). - Testing routes (see angular-testing).

日本語の概要は準備中です。原文の説明を表示しています。

AratKruglik/claude-sdlc362026年9月21日 更新

State management for Angular 18-21: signals (signal/computed/effect), services-as-state, NgRx Store + Effects + Selectors, NgRx Component Store, NgRx Signals (newer signal-based store). RxJS essentials — operators, async pipe, takeUntilDestroyed, signal/observable interop. Use this skill to: - Pick the right state tool (signals / services / NgRx variant / vue-query equivalent). - Use signals correctly (signal/computed/effect — when each). - Build a Pinia-style service-as-state singleton. - Set up NgRx Store + Effects + Selectors. - Use RxJS without leaking subscriptions (async pipe, takeUntilDestroyed, Subject patterns). - Bridge signals ↔ observables via toSignal / toObservable. Do NOT use this skill for: - General Angular conventions (see angular-conventions). - Routing state (see angular-routing). - Form state (see angular-forms). - Testing state (see angular-testing).

日本語の概要は準備中です。原文の説明を表示しています。

AratKruglik/claude-sdlc362026年9月21日 更新

Testing Angular 18-21: TestBed, component harnesses (@angular/cdk/testing), Karma+Jasmine (default historical) vs Jest (jest-preset-angular, modern), Angular Testing Library (RTL-style). HttpClient mocking via HttpTestingController. NgRx Effects testing. Cypress / Playwright e2e. Use this skill to: - Detect runner (Karma+Jasmine vs Jest) and configure correctly. - Write component tests with TestBed. - Use component harnesses for Material / custom UI components. - Mock HttpClient via provideHttpClientTesting + HttpTestingController. - Test signal-based inputs with componentRef.setInput(). - Test NgRx Effects with provideMockActions. Do NOT use this skill for: - General Angular conventions (see angular-conventions). - Routing patterns broadly (see angular-routing — covers testing routes briefly). - Form patterns broadly (see angular-forms).

日本語の概要は準備中です。原文の説明を表示しています。

AratKruglik/claude-sdlc362026年9月21日 更新

Shared conventions for every SDLC development-phase architect agent: hard rules, code quality bar, workflow steps (superpowers invocation, spec reading, codebase exploration, verification), and the report/compact-summary contract. Architects load this skill first, then apply their stack-specific instructions on top.

日本語の概要は準備中です。原文の説明を表示しています。

AratKruglik/claude-sdlc362026年9月21日 更新

AratKruglik のスキルをすべて見る

このスキルの問題を報告する