1. javascript
  2. /typescript
  3. /react

React with TypeScript

TypeScript and React form a powerful combination for building robust, maintainable user interfaces. TypeScript provides static typing for React components, props, state, and event handlers, enabling better developer experience, fewer runtime errors, and improved code documentation.

Using TypeScript with React helps catch common mistakes like passing incorrect props, improper event handling, and state management issues at compile time rather than runtime.

Component Props and Types

Functional Components with Props

import React from 'react';

// Define props interface
interface ButtonProps {
  children: React.ReactNode;
  variant?: 'primary' | 'secondary' | 'danger';
  size?: 'small' | 'medium' | 'large';
  disabled?: boolean;
  onClick?: () => void;
}

// Functional component with typed props
const Button: React.FC<ButtonProps> = ({
  children,
  variant = 'primary',
  size = 'medium',
  disabled = false,
  onClick
}) => {
  return (
    <button
      className={`btn btn-${variant} btn-${size}`}
      disabled={disabled}
      onClick={onClick}
    >
      {children}
    </button>
  );
};

// Usage
const App: React.FC = () => {
  const handleClick = () => {
    console.log('Button clicked!');
  };

  return (
    <div>
      <Button variant="primary" size="large" onClick={handleClick}>
        Click me
      </Button>
      <Button variant="danger" disabled>
        Disabled
      </Button>
    </div>
  );
};

Component Props with Children

// Generic container component
interface ContainerProps {
  children: React.ReactNode;
  className?: string;
  maxWidth?: number;
}

const Container: React.FC<ContainerProps> = ({ children, className, maxWidth }) => {
  const style = maxWidth ? { maxWidth: `${maxWidth}px` } : undefined;
  
  return (
    <div className={`container ${className || ''}`} style={style}>
      {children}
    </div>
  );
};

// Component with render props
interface RenderPropsComponentProps<T> {
  data: T[];
  renderItem: (item: T, index: number) => React.ReactNode;
  loading?: boolean;
}

function List<T>({ data, renderItem, loading }: RenderPropsComponentProps<T>) {
  if (loading) {
    return <div>Loading...</div>;
  }

  return (
    <ul>
      {data.map((item, index) => (
        <li key={index}>{renderItem(item, index)}</li>
      ))}
    </ul>
  );
}

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

const UserList: React.FC = () => {
  const users: User[] = [
    { id: 1, name: 'John Doe', email: '[email protected]' },
    { id: 2, name: 'Jane Smith', email: '[email protected]' }
  ];

  return (
    <List
      data={users}
      renderItem={(user) => (
        <div>
          <strong>{user.name}</strong> - {user.email}
        </div>
      )}
    />
  );
};

React Hooks with TypeScript

useState Hook

import React, { useState } from 'react';

interface User {
  id: number;
  name: string;
  email: string;
}

const UserProfile: React.FC = () => {
  // Primitive state
  const [count, setCount] = useState<number>(0);
  const [name, setName] = useState<string>('');
  const [isLoading, setIsLoading] = useState<boolean>(false);

  // Object state with interface
  const [user, setUser] = useState<User | null>(null);

  // Array state
  const [users, setUsers] = useState<User[]>([]);

  // State with initial value (type inference)
  const [message, setMessage] = useState('Initial message'); // string inferred

  // Complex state with union types
  const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle');

  const updateUser = (updates: Partial<User>) => {
    setUser(prevUser => 
      prevUser ? { ...prevUser, ...updates } : null
    );
  };

  const addUser = (newUser: User) => {
    setUsers(prevUsers => [...prevUsers, newUser]);
  };

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(prev => prev + 1)}>
        Increment
      </button>
      
      {user && (
        <div>
          <h3>{user.name}</h3>
          <p>{user.email}</p>
        </div>
      )}
    </div>
  );
};

useEffect Hook

import React, { useState, useEffect } from 'react';

interface ApiResponse<T> {
  data: T;
  status: number;
  message: string;
}

const DataFetcher: React.FC = () => {
  const [users, setUsers] = useState<User[]>([]);
  const [loading, setLoading] = useState<boolean>(true);
  const [error, setError] = useState<string | null>(null);

  // Effect with cleanup
  useEffect(() => {
    let isCancelled = false;

    const fetchUsers = async () => {
      try {
        setLoading(true);
        setError(null);
        
        const response = await fetch('/api/users');
        const result: ApiResponse<User[]> = await response.json();
        
        if (!isCancelled) {
          setUsers(result.data);
        }
      } catch (err) {
        if (!isCancelled) {
          setError(err instanceof Error ? err.message : 'Unknown error');
        }
      } finally {
        if (!isCancelled) {
          setLoading(false);
        }
      }
    };

    fetchUsers();

    // Cleanup function
    return () => {
      isCancelled = true;
    };
  }, []); // Empty dependency array

  // Effect with dependencies
  useEffect(() => {
    const handleResize = () => {
      console.log('Window resized');
    };

    window.addEventListener('resize', handleResize);
    
    return () => {
      window.removeEventListener('resize', handleResize);
    };
  }, []);

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error}</div>;

  return (
    <div>
      {users.map(user => (
        <div key={user.id}>{user.name}</div>
      ))}
    </div>
  );
};

Custom Hooks

// Custom hook for API calls
function useApi<T>(url: string) {
  const [data, setData] = useState<T | null>(null);
  const [loading, setLoading] = useState<boolean>(true);
  const [error, setError] = useState<string | null>(null);

  const refetch = async () => {
    try {
      setLoading(true);
      setError(null);
      const response = await fetch(url);
      const result: T = await response.json();
      setData(result);
    } catch (err) {
      setError(err instanceof Error ? err.message : 'Unknown error');
    } finally {
      setLoading(false);
    }
  };

  useEffect(() => {
    refetch();
  }, [url]);

  return { data, loading, error, refetch };
}

// Custom hook for form handling
interface FormState<T> {
  values: T;
  errors: Partial<Record<keyof T, string>>;
  touched: Partial<Record<keyof T, boolean>>;
}

function useForm<T>(initialValues: T, validate?: (values: T) => Partial<Record<keyof T, string>>) {
  const [formState, setFormState] = useState<FormState<T>>({
    values: initialValues,
    errors: {},
    touched: {}
  });

  const setValue = (field: keyof T, value: T[keyof T]) => {
    setFormState(prev => ({
      ...prev,
      values: { ...prev.values, [field]: value },
      touched: { ...prev.touched, [field]: true },
      errors: validate 
        ? { ...prev.errors, ...validate({ ...prev.values, [field]: value }) }
        : prev.errors
    }));
  };

  const reset = () => {
    setFormState({
      values: initialValues,
      errors: {},
      touched: {}
    });
  };

  return {
    values: formState.values,
    errors: formState.errors,
    touched: formState.touched,
    setValue,
    reset
  };
}

// Usage of custom hooks
const UserForm: React.FC = () => {
  const { data: users, loading, refetch } = useApi<User[]>('/api/users');
  
  const validateUser = (values: Partial<User>) => {
    const errors: Partial<Record<keyof User, string>> = {};
    if (!values.name) errors.name = 'Name is required';
    if (!values.email) errors.email = 'Email is required';
    return errors;
  };

  const { values, errors, touched, setValue, reset } = useForm<Partial<User>>(
    { name: '', email: '' },
    validateUser
  );

  return (
    <form>
      <input
        value={values.name || ''}
        onChange={(e) => setValue('name', e.target.value)}
        placeholder="Name"
      />
      {touched.name && errors.name && <span>{errors.name}</span>}
      
      <input
        value={values.email || ''}
        onChange={(e) => setValue('email', e.target.value)}
        placeholder="Email"
      />
      {touched.email && errors.email && <span>{errors.email}</span>}
    </form>
  );
};

Event Handlers

import React, { useState } from 'react';

const EventHandlerExample: React.FC = () => {
  const [inputValue, setInputValue] = useState<string>('');
  const [selectedFile, setSelectedFile] = useState<File | null>(null);

  // Input change handler
  const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    setInputValue(event.target.value);
  };

  // Form submission handler
  const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
    event.preventDefault();
    console.log('Form submitted with:', inputValue);
  };

  // Button click handler
  const handleButtonClick = (event: React.MouseEvent<HTMLButtonElement>) => {
    console.log('Button clicked at:', event.clientX, event.clientY);
  };

  // File input handler
  const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    const file = event.target.files?.[0] || null;
    setSelectedFile(file);
  };

  // Keyboard event handler
  const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
    if (event.key === 'Enter') {
      console.log('Enter key pressed');
    }
  };

  // Generic event handler with custom data
  const handleCustomClick = (id: number) => (event: React.MouseEvent) => {
    event.preventDefault();
    console.log('Custom click with ID:', id);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        value={inputValue}
        onChange={handleInputChange}
        onKeyDown={handleKeyDown}
        placeholder="Type something..."
      />
      
      <input
        type="file"
        onChange={handleFileChange}
      />
      
      <button type="button" onClick={handleButtonClick}>
        Click me
      </button>
      
      <button type="button" onClick={handleCustomClick(123)}>
        Custom handler
      </button>
      
      <button type="submit">Submit</button>
    </form>
  );
};

Context API with TypeScript

import React, { createContext, useContext, useReducer, ReactNode } from 'react';

// State and action types
interface AppState {
  user: User | null;
  theme: 'light' | 'dark';
  notifications: Notification[];
}

interface Notification {
  id: string;
  type: 'success' | 'error' | 'warning';
  message: string;
}

type AppAction =
  | { type: 'SET_USER'; payload: User | null }
  | { type: 'SET_THEME'; payload: 'light' | 'dark' }
  | { type: 'ADD_NOTIFICATION'; payload: Notification }
  | { type: 'REMOVE_NOTIFICATION'; payload: string };

// Reducer
const appReducer = (state: AppState, action: AppAction): AppState => {
  switch (action.type) {
    case 'SET_USER':
      return { ...state, user: action.payload };
    case 'SET_THEME':
      return { ...state, theme: action.payload };
    case 'ADD_NOTIFICATION':
      return {
        ...state,
        notifications: [...state.notifications, action.payload]
      };
    case 'REMOVE_NOTIFICATION':
      return {
        ...state,
        notifications: state.notifications.filter(n => n.id !== action.payload)
      };
    default:
      return state;
  }
};

// Context type
interface AppContextType {
  state: AppState;
  dispatch: React.Dispatch<AppAction>;
  // Helper functions
  setUser: (user: User | null) => void;
  setTheme: (theme: 'light' | 'dark') => void;
  addNotification: (notification: Omit<Notification, 'id'>) => void;
  removeNotification: (id: string) => void;
}

// Create context
const AppContext = createContext<AppContextType | undefined>(undefined);

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

export const AppProvider: React.FC<AppProviderProps> = ({ children }) => {
  const initialState: AppState = {
    user: null,
    theme: 'light',
    notifications: []
  };

  const [state, dispatch] = useReducer(appReducer, initialState);

  // Helper functions
  const setUser = (user: User | null) => {
    dispatch({ type: 'SET_USER', payload: user });
  };

  const setTheme = (theme: 'light' | 'dark') => {
    dispatch({ type: 'SET_THEME', payload: theme });
  };

  const addNotification = (notification: Omit<Notification, 'id'>) => {
    const id = Math.random().toString(36).substr(2, 9);
    dispatch({ type: 'ADD_NOTIFICATION', payload: { ...notification, id } });
  };

  const removeNotification = (id: string) => {
    dispatch({ type: 'REMOVE_NOTIFICATION', payload: id });
  };

  const value: AppContextType = {
    state,
    dispatch,
    setUser,
    setTheme,
    addNotification,
    removeNotification
  };

  return <AppContext.Provider value={value}>{children}</AppContext.Provider>;
};

// Custom hook to use context
export const useApp = (): AppContextType => {
  const context = useContext(AppContext);
  if (context === undefined) {
    throw new Error('useApp must be used within an AppProvider');
  }
  return context;
};

// Usage in components
const Header: React.FC = () => {
  const { state, setTheme, setUser } = useApp();

  const toggleTheme = () => {
    setTheme(state.theme === 'light' ? 'dark' : 'light');
  };

  const logout = () => {
    setUser(null);
  };

  return (
    <header className={`header ${state.theme}`}>
      <h1>My App</h1>
      {state.user && <span>Welcome, {state.user.name}!</span>}
      <button onClick={toggleTheme}>Toggle Theme</button>
      {state.user && <button onClick={logout}>Logout</button>}
    </header>
  );
};

Higher-Order Components (HOCs)

import React, { ComponentType } from 'react';

// HOC that adds loading state
interface WithLoadingProps {
  loading: boolean;
}

function withLoading<P extends object>(
  WrappedComponent: ComponentType<P>
): ComponentType<P & WithLoadingProps> {
  return (props: P & WithLoadingProps) => {
    if (props.loading) {
      return <div>Loading...</div>;
    }
    return <WrappedComponent {...props} />;
  };
}

// HOC that adds authentication
interface WithAuthProps {
  user: User | null;
}

function withAuth<P extends object>(
  WrappedComponent: ComponentType<P>
): ComponentType<P & WithAuthProps> {
  return (props: P & WithAuthProps) => {
    if (!props.user) {
      return <div>Please log in to access this content.</div>;
    }
    return <WrappedComponent {...props} />;
  };
}

// Component to be enhanced
interface ProfileProps {
  userId: number;
}

const Profile: React.FC<ProfileProps> = ({ userId }) => {
  return <div>Profile for user {userId}</div>;
};

// Enhanced component
const EnhancedProfile = withAuth(withLoading(Profile));

// Usage
const App: React.FC = () => {
  const [user, setUser] = useState<User | null>(null);
  const [loading, setLoading] = useState(false);

  return (
    <EnhancedProfile
      userId={123}
      user={user}
      loading={loading}
    />
  );
};

Refs and forwardRef

import React, { useRef, forwardRef, useImperativeHandle } from 'react';

// Component with ref forwarding
interface InputProps {
  placeholder?: string;
  onChange?: (value: string) => void;
}

const Input = forwardRef<HTMLInputElement, InputProps>(
  ({ placeholder, onChange }, ref) => {
    return (
      <input
        ref={ref}
        placeholder={placeholder}
        onChange={(e) => onChange?.(e.target.value)}
      />
    );
  }
);

// Component with imperative handle
interface CustomInputHandle {
  focus: () => void;
  clear: () => void;
  getValue: () => string;
}

const CustomInput = forwardRef<CustomInputHandle, InputProps>(
  ({ placeholder, onChange }, ref) => {
    const inputRef = useRef<HTMLInputElement>(null);

    useImperativeHandle(ref, () => ({
      focus: () => {
        inputRef.current?.focus();
      },
      clear: () => {
        if (inputRef.current) {
          inputRef.current.value = '';
          onChange?.('');
        }
      },
      getValue: () => {
        return inputRef.current?.value || '';
      }
    }));

    return (
      <input
        ref={inputRef}
        placeholder={placeholder}
        onChange={(e) => onChange?.(e.target.value)}
      />
    );
  }
);

// Usage
const Parent: React.FC = () => {
  const inputRef = useRef<HTMLInputElement>(null);
  const customInputRef = useRef<CustomInputHandle>(null);

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

  const clearCustomInput = () => {
    customInputRef.current?.clear();
  };

  return (
    <div>
      <Input ref={inputRef} placeholder="Regular input" />
      <button onClick={focusInput}>Focus Input</button>
      
      <CustomInput ref={customInputRef} placeholder="Custom input" />
      <button onClick={clearCustomInput}>Clear Custom Input</button>
    </div>
  );
};

TypeScript with React provides excellent type safety and developer experience. The combination enables building robust applications with better error catching, improved refactoring, and self-documenting code through type definitions.