Claude
Skills
Sign in
Back

UI Interaction

Included with Lifetime
$97 forever

This skill should be used when the user asks to "add client interactivity", "implement form validation", "add event handlers", "use client state", "add Zod validation", "implement React hooks", "add local state", "make component interactive", "add form with validation", "use React Hook Form", or needs guidance on client-side events, form handling, optimistic updates, or when to add "use client" directive.

Design

What this skill does


# UI Interaction for Next.js Applications

## Overview

UI Interaction handles the client-side interactivity layer of Next.js applications. This skill covers adding client-side events, managing local state, implementing form validation with Zod, and using React Hook Form for complex forms.

**Key principles:**
- Only add "use client" when client-side APIs are actually needed
- Use Zod schemas for both client and server validation (single source of truth)
- Prefer Server Components by default; convert to Client Components only for interactivity
- Implement optimistic updates for responsive user experience

## Skill-scoped Context

**Official Documentation:**
- React Hooks: https://react.dev/reference/react/hooks
- Zod Validation: https://zod.dev/
- React Hook Form: https://react-hook-form.com/
- Next.js Client Components: https://nextjs.org/docs/app/building-your-application/rendering/client-components

## When to Add "use client"

Add the "use client" directive only when the component uses:

1. **Event handlers** - onClick, onChange, onSubmit, etc.
2. **React hooks** - useState, useEffect, useRef, useCallback, useMemo
3. **Browser APIs** - window, document, localStorage, navigator
4. **Third-party client libraries** - libraries that require browser context

**Pattern: Minimal Client Boundary**

Keep "use client" components as small as possible:

```tsx
// components/counter-button.tsx
"use client";

import { useState } from "react";

export function CounterButton() {
  const [count, setCount] = useState(0);
  return (
    <button onClick={() => setCount(c => c + 1)}>
      Count: {count}
    </button>
  );
}
```

```tsx
// app/page.tsx (Server Component - no "use client")
import { CounterButton } from "@/components/counter-button";

export default function Page() {
  // Server-side data fetching, no client JS here
  return (
    <main>
      <h1>Welcome</h1>
      <CounterButton /> {/* Client boundary starts here */}
    </main>
  );
}
```

## Workflow

### Step 1: Identify Interactive Elements

Analyze the UI to identify elements requiring client-side behavior:
- Form inputs with validation
- Buttons with click handlers
- Elements with hover/focus states
- Components with local state (toggles, dropdowns, modals)
- Elements requiring browser APIs

### Step 2: Add "use client" Directive

Add the directive at the top of the file, before any imports:

```tsx
"use client";

import { useState } from "react";
// ... rest of imports
```

### Step 3: Implement State Management

Use appropriate React hooks for state:

```tsx
"use client";

import { useState, useCallback } from "react";

export function ToggleButton({ initialState = false }: { initialState?: boolean }) {
  const [isOn, setIsOn] = useState(initialState);

  const toggle = useCallback(() => {
    setIsOn(prev => !prev);
  }, []);

  return (
    <button
      onClick={toggle}
      aria-pressed={isOn}
      className={isOn ? "bg-green-500" : "bg-gray-300"}
    >
      {isOn ? "On" : "Off"}
    </button>
  );
}
```

### Step 4: Add Zod Validation

Define Zod schemas for form validation:

```tsx
import { z } from "zod";

// Define schema once, use for client AND server validation
export const contactFormSchema = z.object({
  name: z.string().min(2, "Name must be at least 2 characters"),
  email: z.string().email("Invalid email address"),
  message: z.string().min(10, "Message must be at least 10 characters"),
});

export type ContactFormData = z.infer<typeof contactFormSchema>;
```

## Form Validation with Zod and React Hook Form

### Complete Form Example

```tsx
"use client";

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

const formSchema = z.object({
  email: z.string().email("Please enter a valid email"),
  password: z.string()
    .min(8, "Password must be at least 8 characters")
    .regex(/[A-Z]/, "Password must contain an uppercase letter")
    .regex(/[0-9]/, "Password must contain a number"),
  confirmPassword: z.string(),
}).refine((data) => data.password === data.confirmPassword, {
  message: "Passwords don't match",
  path: ["confirmPassword"],
});

type FormData = z.infer<typeof formSchema>;

export function SignUpForm({ onSubmit }: { onSubmit: (data: FormData) => Promise<void> }) {
  const {
    register,
    handleSubmit,
    formState: { errors, isSubmitting },
  } = useForm<FormData>({
    resolver: zodResolver(formSchema),
  });

  return (
    <form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
      <div>
        <label htmlFor="email" className="block text-sm font-medium">
          Email
        </label>
        <input
          {...register("email")}
          type="email"
          id="email"
          className="mt-1 block w-full rounded-md border-gray-300"
          aria-invalid={errors.email ? "true" : "false"}
        />
        {errors.email && (
          <p className="mt-1 text-sm text-red-600" role="alert">
            {errors.email.message}
          </p>
        )}
      </div>

      <div>
        <label htmlFor="password" className="block text-sm font-medium">
          Password
        </label>
        <input
          {...register("password")}
          type="password"
          id="password"
          className="mt-1 block w-full rounded-md border-gray-300"
          aria-invalid={errors.password ? "true" : "false"}
        />
        {errors.password && (
          <p className="mt-1 text-sm text-red-600" role="alert">
            {errors.password.message}
          </p>
        )}
      </div>

      <div>
        <label htmlFor="confirmPassword" className="block text-sm font-medium">
          Confirm Password
        </label>
        <input
          {...register("confirmPassword")}
          type="password"
          id="confirmPassword"
          className="mt-1 block w-full rounded-md border-gray-300"
          aria-invalid={errors.confirmPassword ? "true" : "false"}
        />
        {errors.confirmPassword && (
          <p className="mt-1 text-sm text-red-600" role="alert">
            {errors.confirmPassword.message}
          </p>
        )}
      </div>

      <button
        type="submit"
        disabled={isSubmitting}
        className="w-full rounded-md bg-blue-600 px-4 py-2 text-white disabled:opacity-50"
      >
        {isSubmitting ? "Signing up..." : "Sign Up"}
      </button>
    </form>
  );
}
```

## Event Handler Patterns

### Click Handlers

```tsx
"use client";

import { useCallback } from "react";

export function DeleteButton({ itemId, onDelete }: {
  itemId: string;
  onDelete: (id: string) => Promise<void>;
}) {
  const handleDelete = useCallback(async () => {
    if (confirm("Are you sure you want to delete this item?")) {
      await onDelete(itemId);
    }
  }, [itemId, onDelete]);

  return (
    <button
      onClick={handleDelete}
      className="text-red-600 hover:text-red-800"
      aria-label="Delete item"
    >
      Delete
    </button>
  );
}
```

### Keyboard Events

```tsx
"use client";

import { useCallback, KeyboardEvent } from "react";

export function SearchInput({ onSearch }: { onSearch: (query: string) => void }) {
  const handleKeyDown = useCallback((e: KeyboardEvent<HTMLInputElement>) => {
    if (e.key === "Enter") {
      onSearch(e.currentTarget.value);
    }
  }, [onSearch]);

  return (
    <input
      type="search"
      placeholder="Search..."
      onKeyDown={handleKeyDown}
      className="rounded-md border px-4 py-2"
      aria-label="Search"
    />
  );
}
```

## Optimistic Updates Pattern

Provide immediate feedback while server action processes:

```tsx
"use client";

import { useOptimistic, useTransition } from "react";

interface Todo {
  id: string;
  text: string;
  completed: boolean;
}

export function TodoItem({
  todo,
  toggleAction
}: {
  todo: Todo;
  toggleAction: (id: string) => Promise<void>;
}) {
  const [isPending, startTransition] = useTransition();
  const [optimisticTodo, setOptim

Related in Design