Prompt directory
Impulse.directory
HomeBrowseStacks
Sign inSign upCreate an account
    rule

    React hook form best practices

    impulse-lab/react-hook-form-best-practices·v1·updated September 25, 2025
    Leonard Roussard
    Leonard Roussard@lionvsx·Impulse Lab
    Content
    ---
    description: Forms rules and instructions
    globs:
    alwaysApply: false
    ---
    
    # React Hook Form Best Practices
    Standards for using React Hook Form. Auto-included for TSX and JSX files.
    <rule>
    name: react_hook_form_best_practices
    description: Best practices for using React Hook Form. Auto-included for TSX and JSX files.
    globs: ["**/*.{tsx,jsx}"]
    filters:
    - type: file_extension
    pattern: "\.(tsx|jsx)$"
    actions:
    - type: suggest
    message: |
    Follow these React Hook Form best practices:
        1. Form Setup:
            - Use proper form initialization
            - Implement proper validation schema
            - Use proper default values
            - Handle form submission properly
        2. Field Registration:
            - Use register method properly
            - Implement proper field validation
            - Handle field errors properly
            - Use proper field naming
        3. Validation Integration:
            - Use Zod or Yup integration
            - Implement custom validation
            - Handle async validation
            - Use proper error messages
        4. Performance Optimization:
            - Use uncontrolled components
            - Implement proper form watching
            - Handle form state properly
            - Use proper re-render optimization
        5. Form State Management:
            - Handle form submission state
            - Implement proper error handling
            - Use proper form reset
            - Handle form dirty state
    examples:
    - input: |
    // Bad: Not using form validation
    const MyForm = () => {
    const [values, setValues] = useState({});
    return (
    <form onSubmit={(e) => {
    e.preventDefault();
    // handle submit
    }}>
    <input onChange={(e) => setValues({...values, name: e.target.value})} />
    </form>
    );
    };
    - // Good: Using React Hook Form with Zod
    const formSchema = z.object({
    name: z.string().min(2),
    email: z.string().email()
    });
    - const MyForm = () => {
    const form = useForm<z.infer<typeof formSchema>>({
    resolver: zodResolver(formSchema)
    });
    - `return (
      <form onSubmit={form.handleSubmit(onSubmit)}>
        <input {...form.register("name")} />
        {form.formState.errors.name && (
          <span>{form.formState.errors.name.message}</span>
        )}
      </form>
    );
    `
    - };
    output: "Use React Hook Form with proper validation schema"
    - input: |
    // Bad: Using controlled components
    const MyInput = () => {
    const [value, setValue] = useState("");
    return (
    <input
    value={value}
    onChange={(e) => setValue(e.target.value)}
    />
    );
    };
    - // Good: Using uncontrolled components with React Hook Form
    const MyInput = () => {
    const { register, formState: { errors } } = useFormContext();
    return (
    <>
    <input {...register("fieldName")} />
    {errors.fieldName && (
    <span>{errors.fieldName.message}</span>
    )}
    </>
    );
    };
    output: "Use uncontrolled components for better performance"
    metadata:
    priority: high
    version: 1.0
    </rule>
    # Forms rules and instructions
    Best practices for handling forms with React Hook Form in Next.js applications, especially with tRPC.
    <rule>
    name: react_hook_form_best_practices
    description: Best practices for using React Hook Form with tRPC and Next.js. Auto-included for TSX files.
    globs: ["**/*.tsx"]
    filters:
    - type: file_extension
    pattern: "\.tsx$"
    - type: content
    pattern: "(?:useForm|Controller|register|handleSubmit|FormProvider)"
    actions:
    - type: suggest
    message: |
    Follow these React Hook Form best practices:
        1. Form Setup:
            - Use the `useForm` hook with explicit TypeScript typing
            - Define Zod schemas for validation
            - Use zodResolver for form validation
            - Initialize defaultValues directly from suspense queries when possible
            - Set sensible fallback values with the nullish coalescing operator (??)
        2. Form-Server Data Flow:
            - For client components with suspense queries:
    
                - Initialize form directly from query data
                - No need for a useEffect to reset form when data changes
                - Keep initial values in React.useMemo for reset functionality
        3. `// Good pattern for client components with suspense queries
    const { data: settings } = useSuspenseQuery(
      trpc.settings.getSettings.queryOptions()
    );
    
    // Store initial values for reset
    const initialValues = React.useMemo(() => ({
      setting1: settings?.setting1 ?? defaultValue,
      setting2: settings?.setting2 ?? defaultValue,
    }), [settings]);
    
    const { register, handleSubmit, reset } = useForm({
      resolver: zodResolver(formSchema),
      defaultValues: {
        setting1: settings?.setting1 ?? defaultValue,
        setting2: settings?.setting2 ?? defaultValue,
      }
    });
    `
        4. Form Controls:
            - Use register for simple inputs:
            - `<input {...register("fieldName")} />
    `
            - Use Controller for complex components:
            - `<Controller
      name="fieldName"
      control={control}
      render={({ field }) => <Select {...field} />}
    />
    `
            - For controlled components like switches, combine register with watch:
            - `<Switch.Root
      id={`field-id`}
      checked={watch("fieldName")}
      onCheckedChange={(checked) => setValue("fieldName", checked, { shouldDirty: true })}
      {...register("fieldName")}
    />
    `
        5. Form Submission:
            - Use handleSubmit to process form data
            - Clear error state before submission
            - Use try/catch for error handling
            - Reset form with new data after successful submission
            - Invalidate queries with full queryOptions():
        6. `const onSubmit = async (data: FormValues) => {
      setError(null);
      try {
        await updateData(data);
        reset(data); // Reset with new values
        queryClient.invalidateQueries(trpc.data.getData.queryOptions());
      } catch (error) {
        setError(getErrorMessage(error));
      }
    };
    `
        7. Form Reset and Discard:
            - Implement a discard function using stored initial values:
            - `const handleDiscard = () => {
      reset(initialValues);
      setError(null);
    };
    `
            - Disable discard button when form is not dirty:
            - `<Button
      disabled={!isDirty || isSubmitting}
      onClick={handleDiscard}
    >
      Discard
    </Button>
    `
        8. Form State Management:
            - Use formState for tracking form status:
    
                - isDirty - Has the form changed?
                - isSubmitting - Is the form currently submitting?
                - errors - Validation errors
            - Use these states for UI feedback:
            - `<SaveButton
      isDirty={isDirty}
      isPending={isSubmitting}
      type="submit"
    />
    `
        9. Error Handling:
            - Display form-level errors with a global message component
            - Show field-level errors below each input
            - Provide clear error messages from both client and server validation
    examples:
    - input: |
    // Bad: Unnecessary useEffect for form reset with suspense query
    const { data: settings } = useSuspenseQuery(trpc.settings.getSettings.queryOptions());
    - const { register, reset } = useForm({
    defaultValues: initialValues // Defined elsewhere
    });
    - React.useEffect(() => {
    if (settings) {
    reset({
    setting1: settings.setting1,
    setting2: settings.setting2,
    });
    }
    }, [settings, reset]);
    - // Good: Direct initialization with suspense query data
    const { data: settings } = useSuspenseQuery(trpc.settings.getSettings.queryOptions());
    - const initialValues = React.useMemo(() => ({
    setting1: settings?.setting1 ?? defaultValue,
    setting2: settings?.setting2 ?? defaultValue,
    }), [settings]);
    - const { register, handleSubmit, reset } = useForm({
    defaultValues: {
    setting1: settings?.setting1 ?? defaultValue,
    setting2: settings?.setting2 ?? defaultValue,
    }
    });
    output: "Initialize forms directly from suspense query data without unnecessary useEffect"
    - input: |
    // Bad: Using just queryKey for invalidation after form submission
    const onSubmit = async (data: FormValues) => {
    try {
    await updateSettings(data);
    queryClient.invalidateQueries({
    queryKey: trpc.settings.getSettings.queryKey
    });
    } catch (error) {
    // Error handling
    }
    };
    - // Good: Using full queryOptions() for invalidation
    const onSubmit = async (data: FormValues) => {
    try {
    await updateSettings(data);
    reset(data); // Reset with new values
    queryClient.invalidateQueries(trpc.settings.getSettings.queryOptions());
    } catch (error) {
    // Error handling
    }
    };
    output: "Reset form with new data and use full queryOptions() for invalidation after submission"
    - input: |
    // Bad: No proper handling for discard
    const handleDiscard = () => {
    reset();
    };
    - // Good: Proper discard implementation
    const initialValues = React.useMemo(() => ({
    setting1: settings?.setting1 ?? defaultValue,
    setting2: settings?.setting2 ?? defaultValue,
    }), [settings]);
    - const handleDiscard = () => {
    reset(initialValues);
    setError(null);
    };
    - <Button
    disabled={!isDirty || isSubmitting}
    onClick={handleDiscard}
    - `Discard
    `
    - </Button>
    output: "Implement proper discard functionality with stored initial values"
    metadata:
    priority: high
    version: 1.0
    </rule>

    Install with the impulse CLI

    $npx @impulselab/directory impulselab/react-hook-form-best-practices

    Stats

    Views
    6
    Saves
    0

    Owner

    impulse-logo

    Impulse Lab

    Official artifact