Agent Skill
2/7/2026webapp-devreact-components
Use this skill when building React components. Provides patterns for component structure, props, state management, custom hooks, and TypeScript integration.
A
amplifyautomation
1GitHub Stars
1Views
npx skills add AmplifyAutomation/amplify-plugin-marketplace
SKILL.md
| Name | webapp-devreact-components |
| Description | Use this skill when building React components. Provides patterns for component structure, props, state management, custom hooks, and TypeScript integration. |
name: webapp-dev:react-components description: Use this skill when building React components. Provides patterns for component structure, props, state management, custom hooks, and TypeScript integration.
React Component Patterns
Component Structure
Basic Component Template
// components/ui/Button.tsx
import { forwardRef } from 'react'
import { cn } from '@/lib/utils'
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'secondary' | 'outline' | 'ghost'
size?: 'sm' | 'md' | 'lg'
isLoading?: boolean
}
const Button = forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant = 'primary', size = 'md', isLoading, children, disabled, ...props }, ref) => {
return (
<button
ref={ref}
className={cn(
'inline-flex items-center justify-center rounded-md font-medium transition-colors',
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2',
'disabled:pointer-events-none disabled:opacity-50',
{
'bg-blue-600 text-white hover:bg-blue-700': variant === 'primary',
'bg-gray-200 text-gray-900 hover:bg-gray-300': variant === 'secondary',
'border border-gray-300 bg-transparent hover:bg-gray-100': variant === 'outline',
'bg-transparent hover:bg-gray-100': variant === 'ghost',
},
{
'h-8 px-3 text-sm': size === 'sm',
'h-10 px-4 text-base': size === 'md',
'h-12 px-6 text-lg': size === 'lg',
},
className
)}
disabled={disabled || isLoading}
{...props}
>
{isLoading ? (
<>
<Spinner className="mr-2 h-4 w-4" />
Loading...
</>
) : (
children
)}
</button>
)
}
)
Button.displayName = 'Button'
export { Button, type ButtonProps }
Compound Components
// components/ui/Card.tsx
import { cn } from '@/lib/utils'
interface CardProps extends React.HTMLAttributes<HTMLDivElement> {}
function Card({ className, ...props }: CardProps) {
return (
<div
className={cn('rounded-lg border bg-white shadow-sm', className)}
{...props}
/>
)
}
function CardHeader({ className, ...props }: CardProps) {
return (
<div
className={cn('flex flex-col space-y-1.5 p-6', className)}
{...props}
/>
)
}
function CardTitle({ className, ...props }: React.HTMLAttributes<HTMLHeadingElement>) {
return (
<h3
className={cn('text-xl font-semibold leading-none tracking-tight', className)}
{...props}
/>
)
}
function CardDescription({ className, ...props }: React.HTMLAttributes<HTMLParagraphElement>) {
return (
<p
className={cn('text-sm text-gray-500', className)}
{...props}
/>
)
}
function CardContent({ className, ...props }: CardProps) {
return <div className={cn('p-6 pt-0', className)} {...props} />
}
function CardFooter({ className, ...props }: CardProps) {
return (
<div
className={cn('flex items-center p-6 pt-0', className)}
{...props}
/>
)
}
export { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter }
Form Components
Input with Validation
// components/forms/Input.tsx
'use client'
import { forwardRef } from 'react'
import { cn } from '@/lib/utils'
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
label?: string
error?: string
hint?: string
}
const Input = forwardRef<HTMLInputElement, InputProps>(
({ className, label, error, hint, id, ...props }, ref) => {
const inputId = id || label?.toLowerCase().replace(/\s+/g, '-')
return (
<div className="space-y-2">
{label && (
<label
htmlFor={inputId}
className="text-sm font-medium text-gray-700"
>
{label}
</label>
)}
<input
id={inputId}
ref={ref}
className={cn(
'flex h-10 w-full rounded-md border border-gray-300 bg-white px-3 py-2',
'text-sm placeholder:text-gray-400',
'focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent',
'disabled:cursor-not-allowed disabled:opacity-50',
error && 'border-red-500 focus:ring-red-500',
className
)}
aria-invalid={!!error}
aria-describedby={error ? `${inputId}-error` : hint ? `${inputId}-hint` : undefined}
{...props}
/>
{error && (
<p id={`${inputId}-error`} className="text-sm text-red-500">
{error}
</p>
)}
{hint && !error && (
<p id={`${inputId}-hint`} className="text-sm text-gray-500">
{hint}
</p>
)}
</div>
)
}
)
Input.displayName = 'Input'
export { Input }
Form with React Hook Form
// components/forms/LoginForm.tsx
'use client'
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { z } from 'zod'
import { Button } from '@/components/ui/Button'
import { Input } from '@/components/forms/Input'
const loginSchema = z.object({
email: z.string().email('Please enter a valid email'),
password: z.string().min(8, 'Password must be at least 8 characters'),
})
type LoginFormData = z.infer<typeof loginSchema>
interface LoginFormProps {
onSubmit: (data: LoginFormData) => Promise<void>
}
export function LoginForm({ onSubmit }: LoginFormProps) {
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
} = useForm<LoginFormData>({
resolver: zodResolver(loginSchema),
})
return (
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
<Input
label="Email"
type="email"
placeholder="you@example.com"
error={errors.email?.message}
{...register('email')}
/>
<Input
label="Password"
type="password"
placeholder="••••••••"
error={errors.password?.message}
{...register('password')}
/>
<Button type="submit" isLoading={isSubmitting} className="w-full">
Sign In
</Button>
</form>
)
}
Custom Hooks
useAsync Hook
// hooks/useAsync.ts
'use client'
import { useState, useCallback } from 'react'
interface AsyncState<T> {
data: T | null
error: Error | null
isLoading: boolean
}
export function useAsync<T>() {
const [state, setState] = useState<AsyncState<T>>({
data: null,
error: null,
isLoading: false,
})
const execute = useCallback(async (asyncFunction: () => Promise<T>) => {
setState({ data: null, error: null, isLoading: true })
try {
const data = await asyncFunction()
setState({ data, error: null, isLoading: false })
return data
} catch (error) {
setState({ data: null, error: error as Error, isLoading: false })
throw error
}
}, [])
return { ...state, execute }
}
useDebounce Hook
// hooks/useDebounce.ts
'use client'
import { useState, useEffect } from 'react'
export function useDebounce<T>(value: T, delay: number = 500): T {
const [debouncedValue, setDebouncedValue] = useState<T>(value)
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedValue(value)
}, delay)
return () => {
clearTimeout(timer)
}
}, [value, delay])
return debouncedValue
}
useLocalStorage Hook
// hooks/useLocalStorage.ts
'use client'
import { useState, useEffect } from 'react'
export function useLocalStorage<T>(
key: string,
initialValue: T
): [T, (value: T | ((prev: T) => T)) => void] {
const [storedValue, setStoredValue] = useState<T>(initialValue)
useEffect(() => {
try {
const item = window.localStorage.getItem(key)
if (item) {
setStoredValue(JSON.parse(item))
}
} catch (error) {
console.error(`Error reading localStorage key "${key}":`, error)
}
}, [key])
const setValue = (value: T | ((prev: T) => T)) => {
try {
const valueToStore = value instanceof Function ? value(storedValue) : value
setStoredValue(valueToStore)
window.localStorage.setItem(key, JSON.stringify(valueToStore))
} catch (error) {
console.error(`Error setting localStorage key "${key}":`, error)
}
}
return [storedValue, setValue]
}
State Management Patterns
Context with Reducer
// contexts/AuthContext.tsx
'use client'
import { createContext, useContext, useReducer, type ReactNode } from 'react'
interface User {
id: string
email: string
name: string
}
interface AuthState {
user: User | null
isLoading: boolean
}
type AuthAction =
| { type: 'SET_USER'; payload: User }
| { type: 'LOGOUT' }
| { type: 'SET_LOADING'; payload: boolean }
const AuthContext = createContext<{
state: AuthState
dispatch: React.Dispatch<AuthAction>
} | null>(null)
function authReducer(state: AuthState, action: AuthAction): AuthState {
switch (action.type) {
case 'SET_USER':
return { ...state, user: action.payload, isLoading: false }
case 'LOGOUT':
return { ...state, user: null, isLoading: false }
case 'SET_LOADING':
return { ...state, isLoading: action.payload }
default:
return state
}
}
export function AuthProvider({ children }: { children: ReactNode }) {
const [state, dispatch] = useReducer(authReducer, {
user: null,
isLoading: true,
})
return (
<AuthContext.Provider value={{ state, dispatch }}>
{children}
</AuthContext.Provider>
)
}
export function useAuth() {
const context = useContext(AuthContext)
if (!context) {
throw new Error('useAuth must be used within an AuthProvider')
}
return context
}
Loading and Error States
Skeleton Component
// components/ui/Skeleton.tsx
import { cn } from '@/lib/utils'
interface SkeletonProps extends React.HTMLAttributes<HTMLDivElement> {}
export function Skeleton({ className, ...props }: SkeletonProps) {
return (
<div
className={cn('animate-pulse rounded-md bg-gray-200', className)}
{...props}
/>
)
}
// Usage
function UserCardSkeleton() {
return (
<div className="flex items-center space-x-4">
<Skeleton className="h-12 w-12 rounded-full" />
<div className="space-y-2">
<Skeleton className="h-4 w-[200px]" />
<Skeleton className="h-4 w-[150px]" />
</div>
</div>
)
}
Error Boundary
// components/ErrorBoundary.tsx
'use client'
import { Component, type ReactNode } from 'react'
interface Props {
children: ReactNode
fallback?: ReactNode
}
interface State {
hasError: boolean
error: Error | null
}
export class ErrorBoundary extends Component<Props, State> {
constructor(props: Props) {
super(props)
this.state = { hasError: false, error: null }
}
static getDerivedStateFromError(error: Error): State {
return { hasError: true, error }
}
render() {
if (this.state.hasError) {
return this.props.fallback || (
<div className="flex flex-col items-center justify-center p-8">
<h2 className="text-xl font-semibold text-gray-900">
Something went wrong
</h2>
<p className="text-gray-500 mt-2">
{this.state.error?.message}
</p>
<button
onClick={() => this.setState({ hasError: false, error: null })}
className="mt-4 px-4 py-2 bg-blue-600 text-white rounded-md"
>
Try again
</button>
</div>
)
}
return this.props.children
}
}
Accessibility Patterns
Focus Trap
// hooks/useFocusTrap.ts
'use client'
import { useEffect, useRef } from 'react'
export function useFocusTrap<T extends HTMLElement>() {
const containerRef = useRef<T>(null)
useEffect(() => {
const container = containerRef.current
if (!container) return
const focusableElements = container.querySelectorAll<HTMLElement>(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
)
const firstElement = focusableElements[0]
const lastElement = focusableElements[focusableElements.length - 1]
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key !== 'Tab') return
if (e.shiftKey && document.activeElement === firstElement) {
e.preventDefault()
lastElement?.focus()
} else if (!e.shiftKey && document.activeElement === lastElement) {
e.preventDefault()
firstElement?.focus()
}
}
container.addEventListener('keydown', handleKeyDown)
firstElement?.focus()
return () => {
container.removeEventListener('keydown', handleKeyDown)
}
}, [])
return containerRef
}
Screen Reader Only
// components/ui/ScreenReaderOnly.tsx
export function ScreenReaderOnly({ children }: { children: React.ReactNode }) {
return (
<span className="sr-only">
{children}
</span>
)
}
// Tailwind CSS class
// .sr-only {
// position: absolute;
// width: 1px;
// height: 1px;
// padding: 0;
// margin: -1px;
// overflow: hidden;
// clip: rect(0, 0, 0, 0);
// white-space: nowrap;
// border-width: 0;
// }
TypeScript Utilities
Utility Functions
// lib/utils.ts
import { type ClassValue, clsx } from 'clsx'
import { twMerge } from 'tailwind-merge'
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
// Type helpers
export type PropsWithClassName<P = unknown> = P & {
className?: string
}
export type Prettify<T> = {
[K in keyof T]: T[K]
} & {}
Common Type Patterns
// types/index.ts
// API Response wrapper
export interface ApiResponse<T> {
data: T
error: string | null
status: 'success' | 'error'
}
// Pagination
export interface PaginatedResponse<T> {
items: T[]
total: number
page: number
pageSize: number
hasMore: boolean
}
// Form state
export interface FormState {
isSubmitting: boolean
isSuccess: boolean
error: string | null
}
// Async data state
export type AsyncData<T> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: Error }
Skills Info
Original Name:webapp-devreact-componentsAuthor:amplifyautomation
Download