TechLead
Lesson 8 of 8
5 min read
TypeScript

TypeScript with React

Type React components, props, hooks, and events for type-safe React applications

Typing Components

import React from 'react';

// Function component with props interface
interface GreetingProps {
  name: string;
  age?: number;  // optional
}

function Greeting({ name, age }: GreetingProps) {
  return (
    <div>
      Hello, {name}!
      {age && <span> You are {age} years old.</span>}
    </div>
  );
}

// Arrow function component
const Welcome: React.FC<GreetingProps> = ({ name, age }) => {
  return <h1>Welcome, {name}!</h1>;
};

// Component with children
interface CardProps {
  title: string;
  children: React.ReactNode;
}

function Card({ title, children }: CardProps) {
  return (
    <div className="card">
      <h2>{title}</h2>
      {children}
    </div>
  );
}

// Usage
<Card title="My Card">
  <p>Card content</p>
</Card>

Typing Props

// Props with various types
interface ButtonProps {
  label: string;
  onClick: () => void;
  variant?: 'primary' | 'secondary' | 'danger';
  disabled?: boolean;
  icon?: React.ReactNode;
  className?: string;
}

function Button({
  label,
  onClick,
  variant = 'primary',
  disabled = false,
  icon,
  className
}: ButtonProps) {
  return (
    <button
      onClick={onClick}
      disabled={disabled}
      className={`btn btn-${variant} ${className || ''}`}
    >
      {icon && <span className="icon">{icon}</span>}
      {label}
    </button>
  );
}

// Props extending HTML attributes
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
  label: string;
  error?: string;
}

function Input({ label, error, ...inputProps }: InputProps) {
  return (
    <div>
      <label>{label}</label>
      <input {...inputProps} />
      {error && <span className="error">{error}</span>}
    </div>
  );
}

// Render props pattern
interface DataFetcherProps<T> {
  url: string;
  render: (data: T | null, loading: boolean) => React.ReactNode;
}

Typing Hooks

import { useState, useEffect, useRef, useCallback, useMemo } from 'react';

// useState with type inference
const [count, setCount] = useState(0);  // inferred as number

// useState with explicit type
const [user, setUser] = useState<User | null>(null);

// useState with complex state
interface FormState {
  name: string;
  email: string;
  age: number;
}

const [form, setForm] = useState<FormState>({
  name: '',
  email: '',
  age: 0
});

// useRef
const inputRef = useRef<HTMLInputElement>(null);
const countRef = useRef<number>(0);

// useCallback with typed parameters
const handleClick = useCallback((id: number) => {
  console.log('Clicked:', id);
}, []);

// useMemo with return type
const expensiveValue = useMemo<number>(() => {
  return someExpensiveCalculation();
}, [dependency]);

// useEffect with cleanup
useEffect(() => {
  const handler = (e: KeyboardEvent) => {
    console.log(e.key);
  };

  window.addEventListener('keydown', handler);

  return () => {
    window.removeEventListener('keydown', handler);
  };
}, []);

Custom Hooks

// Custom hook with generics
function useLocalStorage<T>(key: string, initialValue: T) {
  const [storedValue, setStoredValue] = useState<T>(() => {
    try {
      const item = localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch {
      return initialValue;
    }
  });

  const setValue = (value: T | ((val: T) => T)) => {
    const valueToStore = value instanceof Function ? value(storedValue) : value;
    setStoredValue(valueToStore);
    localStorage.setItem(key, JSON.stringify(valueToStore));
  };

  return [storedValue, setValue] as const;
}

// Usage
const [name, setName] = useLocalStorage<string>('name', '');

// Fetch hook
interface UseFetchResult<T> {
  data: T | null;
  loading: boolean;
  error: Error | null;
}

function useFetch<T>(url: string): UseFetchResult<T> {
  const [data, setData] = useState<T | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);

  useEffect(() => {
    fetch(url)
      .then(res => res.json())
      .then(setData)
      .catch(setError)
      .finally(() => setLoading(false));
  }, [url]);

  return { data, loading, error };
}

// Usage
const { data: users, loading } = useFetch<User[]>('/api/users');

Event Handling

// Mouse events
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
  console.log(e.currentTarget);
};

// Form events
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
  e.preventDefault();
  // handle form submission
};

// Input events
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
  setName(e.target.value);
};

// Select events
const handleSelect = (e: React.ChangeEvent<HTMLSelectElement>) => {
  setOption(e.target.value);
};

// Keyboard events
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
  if (e.key === 'Enter') {
    handleSubmit();
  }
};

// Focus events
const handleFocus = (e: React.FocusEvent<HTMLInputElement>) => {
  e.target.select();
};

// Drag events
const handleDrop = (e: React.DragEvent<HTMLDivElement>) => {
  e.preventDefault();
  const files = e.dataTransfer.files;
};

Context API

import { createContext, useContext, useState, ReactNode } from 'react';

// Define context type
interface ThemeContextType {
  theme: 'light' | 'dark';
  toggleTheme: () => void;
}

// Create context with default value
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);

// Provider component
interface ThemeProviderProps {
  children: ReactNode;
}

function ThemeProvider({ children }: ThemeProviderProps) {
  const [theme, setTheme] = useState<'light' | 'dark'>('light');

  const toggleTheme = () => {
    setTheme(prev => prev === 'light' ? 'dark' : 'light');
  };

  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

// Custom hook for context
function useTheme(): ThemeContextType {
  const context = useContext(ThemeContext);
  if (context === undefined) {
    throw new Error('useTheme must be used within a ThemeProvider');
  }
  return context;
}

// Usage
function ThemedButton() {
  const { theme, toggleTheme } = useTheme();
  return (
    <button onClick={toggleTheme}>
      Current theme: {theme}
    </button>
  );
}

Generic Components

// Generic list component
interface ListProps<T> {
  items: T[];
  renderItem: (item: T, index: number) => React.ReactNode;
  keyExtractor: (item: T) => string;
}

function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
  return (
    <ul>
      {items.map((item, index) => (
        <li key={keyExtractor(item)}>
          {renderItem(item, index)}
        </li>
      ))}
    </ul>
  );
}

// Usage
interface User {
  id: string;
  name: string;
}

const users: User[] = [
  { id: '1', name: 'Alice' },
  { id: '2', name: 'Bob' }
];

<List
  items={users}
  renderItem={(user) => <span>{user.name}</span>}
  keyExtractor={(user) => user.id}
/>

Key Takeaways

  • • Define interfaces for component props
  • • Use generic types for hooks like useState
  • • Type event handlers with React event types
  • • Create type-safe custom hooks
  • • Use generic components for reusability
  • • Type Context API with proper null checks

Continue Learning