ui interaction

UI Interaction for Next.js Applications

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "ui interaction" with this command: npx skills add constellos/claude-code-plugins/constellos-claude-code-plugins-ui-interaction

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:

When to Add "use client"

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

  • Event handlers - onClick, onChange, onSubmit, etc.

  • React hooks - useState, useEffect, useRef, useCallback, useMemo

  • Browser APIs - window, document, localStorage, navigator

  • Third-party client libraries - libraries that require browser context

Pattern: Minimal Client Boundary

Keep "use client" components as small as possible:

// 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> ); }

// 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:

"use client";

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

Step 3: Implement State Management

Use appropriate React hooks for state:

"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:

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

"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>

  &#x3C;div>
    &#x3C;label htmlFor="password" className="block text-sm font-medium">
      Password
    &#x3C;/label>
    &#x3C;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 &#x26;&#x26; (
      &#x3C;p className="mt-1 text-sm text-red-600" role="alert">
        {errors.password.message}
      &#x3C;/p>
    )}
  &#x3C;/div>

  &#x3C;div>
    &#x3C;label htmlFor="confirmPassword" className="block text-sm font-medium">
      Confirm Password
    &#x3C;/label>
    &#x3C;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 &#x26;&#x26; (
      &#x3C;p className="mt-1 text-sm text-red-600" role="alert">
        {errors.confirmPassword.message}
      &#x3C;/p>
    )}
  &#x3C;/div>

  &#x3C;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"}
  &#x3C;/button>
&#x3C;/form>

); }

Event Handler Patterns

Click Handlers

"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

"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:

"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, setOptimisticTodo] = useOptimistic( todo, (state, completed: boolean) => ({ ...state, completed }) );

const handleToggle = () => { startTransition(async () => { setOptimisticTodo(!optimisticTodo.completed); await toggleAction(todo.id); }); };

return ( <div className={isPending ? "opacity-50" : ""}> <input type="checkbox" checked={optimisticTodo.completed} onChange={handleToggle} aria-label={Mark "${todo.text}" as ${optimisticTodo.completed ? "incomplete" : "complete"}} /> <span className={optimisticTodo.completed ? "line-through" : ""}> {todo.text} </span> </div> ); }

Local State Patterns

Toggle State

"use client";

import { useState, useCallback } from "react";

export function Accordion({ title, children }: { title: string; children: React.ReactNode; }) { const [isOpen, setIsOpen] = useState(false);

const toggle = useCallback(() => setIsOpen(prev => !prev), []);

return ( <div className="border rounded-md"> <button onClick={toggle} aria-expanded={isOpen} className="w-full px-4 py-2 text-left font-medium" > {title} <span className="float-right">{isOpen ? "−" : "+"}</span> </button> {isOpen && ( <div className="px-4 py-2 border-t"> {children} </div> )} </div> ); }

Controlled Input

"use client";

import { useState, ChangeEvent, useCallback } from "react";

export function ControlledInput({ initialValue = "", onChange }: { initialValue?: string; onChange?: (value: string) => void; }) { const [value, setValue] = useState(initialValue);

const handleChange = useCallback((e: ChangeEvent<HTMLInputElement>) => { const newValue = e.target.value; setValue(newValue); onChange?.(newValue); }, [onChange]);

return ( <input type="text" value={value} onChange={handleChange} className="rounded-md border px-4 py-2" /> ); }

Best Practices

DO:

  • Keep "use client" components minimal and focused

  • Define Zod schemas in shared files for client AND server use

  • Use React Hook Form for complex forms with multiple fields

  • Implement optimistic updates for better UX

  • Add proper aria attributes for accessibility

  • Use useCallback for stable function references passed to children

DON'T:

  • Add "use client" to components that don't need it

  • Duplicate validation logic between client and server

  • Use client-side state for data that should come from the server

  • Forget to handle loading and error states

  • Skip accessibility attributes on interactive elements

Quick Reference

When to Use Each Hook

Hook Use Case

useState Local component state

useCallback Memoize event handlers

useMemo Expensive computations

useRef DOM references, mutable values

useOptimistic Optimistic UI updates

useTransition Non-blocking state updates

Zod Common Validators

Validator Example

string z.string().min(1).max(100)

email z.string().email()

number z.number().int().positive()

enum z.enum(["a", "b", "c"])

optional z.string().optional()

nullable z.string().nullable()

refine schema.refine(val => condition, "message")

Implementation Workflow

To add interactivity to a component:

  • Identify which elements need client-side behavior

  • Extract interactive elements to separate "use client" component

  • Keep parent component as Server Component when possible

  • Define Zod schemas for any form validation

  • Implement React Hook Form for forms with multiple fields

  • Add event handlers with useCallback for stability

  • Implement optimistic updates for mutations

  • Add proper accessibility attributes

  • Test interactive behavior across devices

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

feature-sliced design

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

ui wireframing

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

supabase local development

No summary provided by upstream source.

Repository SourceNeeds Review