skills/ehtbanton/claudeskillsrepo/react-component-generator

react-component-generator

SKILL.md

React Component Generator

Generate production-ready React components with TypeScript, proper typing, hooks patterns, and various styling approaches.

Output Requirements

File Output: .tsx files, optionally with .css/.module.css/.styles.ts Format: TypeScript React (TSX) Standards: React 18+, TypeScript 5+

When Invoked

Immediately generate a complete, typed React component. Include props interface, sensible defaults, and appropriate hooks.

Component Patterns

Functional Component (Standard)

// Button.tsx
import { type ButtonHTMLAttributes, forwardRef } from 'react';
import styles from './Button.module.css';

export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
  /** Button visual variant */
  variant?: 'primary' | 'secondary' | 'outline' | 'ghost';
  /** Button size */
  size?: 'sm' | 'md' | 'lg';
  /** Show loading state */
  isLoading?: boolean;
  /** Full width button */
  fullWidth?: boolean;
}

export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
  (
    {
      variant = 'primary',
      size = 'md',
      isLoading = false,
      fullWidth = false,
      disabled,
      className,
      children,
      ...props
    },
    ref
  ) => {
    const classNames = [
      styles.button,
      styles[variant],
      styles[size],
      fullWidth && styles.fullWidth,
      isLoading && styles.loading,
      className,
    ]
      .filter(Boolean)
      .join(' ');

    return (
      <button
        ref={ref}
        className={classNames}
        disabled={disabled || isLoading}
        {...props}
      >
        {isLoading ? (
          <>
            <span className={styles.spinner} aria-hidden="true" />
            <span className={styles.srOnly}>Loading...</span>
          </>
        ) : (
          children
        )}
      </button>
    );
  }
);

Button.displayName = 'Button';

Component with State and Effects

// SearchInput.tsx
import {
  useState,
  useEffect,
  useCallback,
  type ChangeEvent,
  type KeyboardEvent,
} from 'react';
import { useDebouncedValue } from '@/hooks/useDebouncedValue';
import styles from './SearchInput.module.css';

export interface SearchInputProps {
  /** Placeholder text */
  placeholder?: string;
  /** Initial search value */
  defaultValue?: string;
  /** Debounce delay in milliseconds */
  debounceMs?: number;
  /** Called when search value changes (debounced) */
  onSearch: (query: string) => void;
  /** Called on Enter key press */
  onSubmit?: (query: string) => void;
  /** Show loading indicator */
  isLoading?: boolean;
  /** Disable input */
  disabled?: boolean;
}

export function SearchInput({
  placeholder = 'Search...',
  defaultValue = '',
  debounceMs = 300,
  onSearch,
  onSubmit,
  isLoading = false,
  disabled = false,
}: SearchInputProps) {
  const [value, setValue] = useState(defaultValue);
  const debouncedValue = useDebouncedValue(value, debounceMs);

  // Call onSearch when debounced value changes
  useEffect(() => {
    onSearch(debouncedValue);
  }, [debouncedValue, onSearch]);

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

  const handleKeyDown = useCallback(
    (e: KeyboardEvent<HTMLInputElement>) => {
      if (e.key === 'Enter' && onSubmit) {
        onSubmit(value);
      }
    },
    [value, onSubmit]
  );

  const handleClear = useCallback(() => {
    setValue('');
  }, []);

  return (
    <div className={styles.container}>
      <input
        type="search"
        className={styles.input}
        placeholder={placeholder}
        value={value}
        onChange={handleChange}
        onKeyDown={handleKeyDown}
        disabled={disabled}
        aria-label="Search"
      />

      {isLoading && (
        <span className={styles.spinner} aria-hidden="true" />
      )}

      {value && !isLoading && (
        <button
          type="button"
          className={styles.clearButton}
          onClick={handleClear}
          aria-label="Clear search"
        >
          ×
        </button>
      )}
    </div>
  );
}

Data Fetching Component

// UserProfile.tsx
import { useQuery } from '@tanstack/react-query';
import { fetchUser } from '@/api/users';
import { Skeleton } from '@/components/Skeleton';
import { ErrorMessage } from '@/components/ErrorMessage';
import styles from './UserProfile.module.css';

export interface UserProfileProps {
  /** User ID to fetch */
  userId: string;
  /** Show compact version */
  compact?: boolean;
}

export function UserProfile({ userId, compact = false }: UserProfileProps) {
  const {
    data: user,
    isLoading,
    isError,
    error,
    refetch,
  } = useQuery({
    queryKey: ['user', userId],
    queryFn: () => fetchUser(userId),
    staleTime: 5 * 60 * 1000, // 5 minutes
  });

  if (isLoading) {
    return <UserProfileSkeleton compact={compact} />;
  }

  if (isError) {
    return (
      <ErrorMessage
        message={error instanceof Error ? error.message : 'Failed to load user'}
        onRetry={refetch}
      />
    );
  }

  if (!user) {
    return <ErrorMessage message="User not found" />;
  }

  if (compact) {
    return (
      <div className={styles.compact}>
        <img
          src={user.avatarUrl}
          alt={user.name}
          className={styles.avatarSmall}
        />
        <span className={styles.name}>{user.name}</span>
      </div>
    );
  }

  return (
    <article className={styles.profile}>
      <header className={styles.header}>
        <img
          src={user.avatarUrl}
          alt={user.name}
          className={styles.avatar}
        />
        <div className={styles.info}>
          <h2 className={styles.name}>{user.name}</h2>
          <p className={styles.email}>{user.email}</p>
        </div>
      </header>

      {user.bio && (
        <p className={styles.bio}>{user.bio}</p>
      )}

      <footer className={styles.footer}>
        <span>Joined {new Date(user.createdAt).toLocaleDateString()}</span>
      </footer>
    </article>
  );
}

function UserProfileSkeleton({ compact }: { compact: boolean }) {
  if (compact) {
    return (
      <div className={styles.compact}>
        <Skeleton circle width={32} height={32} />
        <Skeleton width={100} height={16} />
      </div>
    );
  }

  return (
    <div className={styles.profile}>
      <div className={styles.header}>
        <Skeleton circle width={80} height={80} />
        <div className={styles.info}>
          <Skeleton width={150} height={24} />
          <Skeleton width={200} height={16} />
        </div>
      </div>
      <Skeleton width="100%" height={60} />
    </div>
  );
}

Form Component

// ContactForm.tsx
import { useForm, type SubmitHandler } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import { Button } from '@/components/Button';
import { Input } from '@/components/Input';
import { Textarea } from '@/components/Textarea';
import styles from './ContactForm.module.css';

const contactSchema = z.object({
  name: z.string().min(2, 'Name must be at least 2 characters'),
  email: z.string().email('Please enter a valid email'),
  subject: z.string().min(5, 'Subject must be at least 5 characters'),
  message: z.string().min(20, 'Message must be at least 20 characters'),
});

type ContactFormData = z.infer<typeof contactSchema>;

export interface ContactFormProps {
  /** Called on successful submission */
  onSubmit: (data: ContactFormData) => Promise<void>;
  /** Called on cancel */
  onCancel?: () => void;
}

export function ContactForm({ onSubmit, onCancel }: ContactFormProps) {
  const {
    register,
    handleSubmit,
    reset,
    formState: { errors, isSubmitting, isSubmitSuccessful },
  } = useForm<ContactFormData>({
    resolver: zodResolver(contactSchema),
    defaultValues: {
      name: '',
      email: '',
      subject: '',
      message: '',
    },
  });

  const handleFormSubmit: SubmitHandler<ContactFormData> = async (data) => {
    await onSubmit(data);
    reset();
  };

  if (isSubmitSuccessful) {
    return (
      <div className={styles.success}>
        <p>Thank you! Your message has been sent.</p>
        <Button onClick={() => reset()}>Send another message</Button>
      </div>
    );
  }

  return (
    <form
      className={styles.form}
      onSubmit={handleSubmit(handleFormSubmit)}
      noValidate
    >
      <div className={styles.field}>
        <Input
          label="Name"
          {...register('name')}
          error={errors.name?.message}
          required
        />
      </div>

      <div className={styles.field}>
        <Input
          label="Email"
          type="email"
          {...register('email')}
          error={errors.email?.message}
          required
        />
      </div>

      <div className={styles.field}>
        <Input
          label="Subject"
          {...register('subject')}
          error={errors.subject?.message}
          required
        />
      </div>

      <div className={styles.field}>
        <Textarea
          label="Message"
          rows={5}
          {...register('message')}
          error={errors.message?.message}
          required
        />
      </div>

      <div className={styles.actions}>
        {onCancel && (
          <Button type="button" variant="ghost" onClick={onCancel}>
            Cancel
          </Button>
        )}
        <Button type="submit" isLoading={isSubmitting}>
          Send Message
        </Button>
      </div>
    </form>
  );
}

Modal/Dialog Component

// Modal.tsx
import {
  useEffect,
  useCallback,
  useRef,
  type ReactNode,
  type KeyboardEvent,
} from 'react';
import { createPortal } from 'react-dom';
import { FocusTrap } from '@/components/FocusTrap';
import styles from './Modal.module.css';

export interface ModalProps {
  /** Whether modal is open */
  isOpen: boolean;
  /** Called when modal should close */
  onClose: () => void;
  /** Modal title */
  title: string;
  /** Modal content */
  children: ReactNode;
  /** Modal size */
  size?: 'sm' | 'md' | 'lg' | 'xl';
  /** Close on overlay click */
  closeOnOverlayClick?: boolean;
  /** Close on Escape key */
  closeOnEscape?: boolean;
  /** Show close button */
  showCloseButton?: boolean;
}

export function Modal({
  isOpen,
  onClose,
  title,
  children,
  size = 'md',
  closeOnOverlayClick = true,
  closeOnEscape = true,
  showCloseButton = true,
}: ModalProps) {
  const modalRef = useRef<HTMLDivElement>(null);

  // Handle Escape key
  useEffect(() => {
    if (!isOpen || !closeOnEscape) return;

    const handleKeyDown = (e: globalThis.KeyboardEvent) => {
      if (e.key === 'Escape') {
        onClose();
      }
    };

    document.addEventListener('keydown', handleKeyDown);
    return () => document.removeEventListener('keydown', handleKeyDown);
  }, [isOpen, closeOnEscape, onClose]);

  // Lock body scroll when open
  useEffect(() => {
    if (isOpen) {
      const originalOverflow = document.body.style.overflow;
      document.body.style.overflow = 'hidden';
      return () => {
        document.body.style.overflow = originalOverflow;
      };
    }
  }, [isOpen]);

  const handleOverlayClick = useCallback(() => {
    if (closeOnOverlayClick) {
      onClose();
    }
  }, [closeOnOverlayClick, onClose]);

  const handleContentClick = useCallback((e: React.MouseEvent) => {
    e.stopPropagation();
  }, []);

  if (!isOpen) return null;

  return createPortal(
    <div
      className={styles.overlay}
      onClick={handleOverlayClick}
      role="dialog"
      aria-modal="true"
      aria-labelledby="modal-title"
    >
      <FocusTrap>
        <div
          ref={modalRef}
          className={`${styles.modal} ${styles[size]}`}
          onClick={handleContentClick}
        >
          <header className={styles.header}>
            <h2 id="modal-title" className={styles.title}>
              {title}
            </h2>
            {showCloseButton && (
              <button
                type="button"
                className={styles.closeButton}
                onClick={onClose}
                aria-label="Close modal"
              >
                ×
              </button>
            )}
          </header>
          <div className={styles.content}>{children}</div>
        </div>
      </FocusTrap>
    </div>,
    document.body
  );
}

// Subcomponents for composition
Modal.Footer = function ModalFooter({
  children,
}: {
  children: ReactNode;
}) {
  return <footer className={styles.footer}>{children}</footer>;
};

File Structure Patterns

Single File Component

components/
  Button/
    Button.tsx
    Button.module.css
    Button.test.tsx
    index.ts

index.ts (Barrel Export)

// components/Button/index.ts
export { Button } from './Button';
export type { ButtonProps } from './Button';

Props Patterns

Extending HTML Elements

interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
  variant?: 'primary' | 'secondary';
}

interface InputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'size'> {
  size?: 'sm' | 'md' | 'lg';
}

Polymorphic Components

type AsProp<C extends ElementType> = {
  as?: C;
};

type PropsToOmit<C extends ElementType, P> = keyof (AsProp<C> & P);

type PolymorphicComponentProp<
  C extends ElementType,
  Props = {}
> = React.PropsWithChildren<Props & AsProp<C>> &
  Omit<React.ComponentPropsWithoutRef<C>, PropsToOmit<C, Props>>;

Validation Checklist

Before outputting, verify:

  • TypeScript types for all props
  • Default values for optional props
  • Proper event handler types
  • forwardRef for components needing refs
  • displayName set for forwardRef components
  • Accessible (aria labels, roles)
  • Keyboard navigation support
  • Loading/error states handled

Example Invocations

Prompt: "Create a React dropdown select component with TypeScript" Output: Complete Select.tsx with props interface, options, keyboard nav, accessibility.

Prompt: "Generate a data table component with sorting and pagination" Output: Complete DataTable.tsx with generic typing, sort handlers, pagination state.

Prompt: "React modal component with animations" Output: Complete Modal.tsx with portal, focus trap, transitions, accessibility.

Weekly Installs
1
First Seen
6 days ago
Installed on
zencoder1
amp1
cline1
openclaw1
opencode1
cursor1