Skip to content
intermediate Phase 4 · TypeScript with React

Typed React Components

Type component props, children, and event handlers in React.

1h 15m
0 problems
Topic Progress 0%

Typing Component Props

Typing Component Props

Interface for Props

interface ButtonProps {
  label: string;
  onClick: () => void;
  variant?: 'primary' | 'secondary' | 'danger';
  disabled?: boolean;
  size?: 'sm' | 'md' | 'lg';
}

function Button({ label, onClick, variant = 'primary', disabled, size = 'md' }: ButtonProps) {
  return (
    <button
      onClick={onClick}
      disabled={disabled}
      className={`btn btn-${variant} btn-${size}`}
    >
      {label}
    </button>
  );
}

// Usage
<Button label="Click me" onClick={() => {}} variant="danger" />

Type vs Interface for Props

// Type alias for props (works the same)
type CardProps = {
  title: string;
  content: string;
  footer?: React.ReactNode;
};

function Card({ title, content, footer }: CardProps) {
  return (
    <div className="card">
      <h2>{title}</h2>
      <p>{content}</p>
      {footer && <div className="card-footer">{footer}</div>}
    </div>
  );
}

Children and Render Props

Children and Render Props

Children Prop

import { ReactNode } from 'react';

interface ContainerProps {
  children: ReactNode;
  title?: string;
}

function Container({ children, title }: ContainerProps) {
  return (
    <div className="container">
      {title && <h1>{title}</h1>}
      {children}
    </div>
  );
}

// Usage
<Container title="Dashboard">
  <p>This is content inside the container.</p>
</Container>

Render Props Pattern

interface MouseTrackerProps {
  render: (position: { x: number; y: number }) => ReactNode;
}

function MouseTracker({ render }: MouseTrackerProps) {
  const [position, setPosition] = useState({ x: 0, y: 0 });

  const handleMouseMove = (e: React.MouseEvent<HTMLDivElement>) => {
    setPosition({ x: e.clientX, y: e.clientY });
  };

  return (
    <div onMouseMove={handleMouseMove} style={{ height: '100vh' }}>
      {render(position)}
    </div>
  );
}

// Usage
<MouseTracker render={({ x, y }) => <span>Mouse at {x}, {y}</span>} />

Event Handlers

Event Handlers

Synthetic Events

import { MouseEvent, ChangeEvent, FormEvent } from 'react';

interface FormProps {
  onSubmit: (data: FormData) => void;
}

function Form({ onSubmit }: FormProps) {
  const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    const formData = new FormData(e.currentTarget);
    onSubmit(formData);
  };

  const handleClick = (e: MouseEvent<HTMLButtonElement>) => {
    console.log('Button clicked:', e.currentTarget.textContent);
  };

  const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
    console.log('Input value:', e.target.value);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input onChange={handleChange} />
      <button type="submit" onClick={handleClick}>Submit</button>
    </form>
  );
}

Generic Event Types

interface ListProps<T> {
  items: T[];
  renderItem: (item: T, index: number) => ReactNode;
  onSelect: (item: T) => void;
}

function List<T extends { id: string | number }>({ items, renderItem, onSelect }: ListProps<T>) {
  return (
    <ul>
      {items.map((item, index) => (
        <li key={item.id} onClick={() => onSelect(item)}>
          {renderItem(item, index)}
        </li>
      ))}
    </ul>
  );
}

Refs and Forwarding

Refs and Forwarding

useRef Typing

import { useRef } from 'react';

function TextInput() {
  // DOM ref
  const inputRef = useRef<HTMLInputElement>(null);

  const focusInput = () => {
    inputRef.current?.focus();
  };

  return (
    <>
      <input ref={inputRef} type="text" />
      <button onClick={focusInput}>Focus</button>
    </>
  );
}

// Mutable ref for non-DOM values
function Stopwatch() {
  const timerRef = useRef<number | null>(null);
  const [time, setTime] = useState(0);

  const start = () => {
    timerRef.current = window.setInterval(() => {
      setTime(t => t + 1);
    }, 1000);
  };

  const stop = () => {
    if (timerRef.current) {
      clearInterval(timerRef.current);
    }
  };

  return <div>{time}s</div>;
}

forwardRef

import { forwardRef } from 'react';

interface InputProps {
  label: string;
  error?: string;
}

const Input = forwardRef<HTMLInputElement, InputProps>(
  ({ label, error, ...props }, ref) => (
    <div>
      <label>{label}</label>
      <input ref={ref} {...props} />
      {error && <span className="error">{error}</span>}
    </div>
  )
);

Input.displayName = 'Input';