Skip to content
Go back

React TypeScript Best Practices: Advanced Component Development

React TypeScript Best Practices: Advanced Component Development

React with TypeScript provides excellent type safety and developer experience for building scalable frontend applications. This comprehensive guide covers advanced component patterns, custom hooks, performance optimization, and production-ready development practices.

Why Use React with TypeScript?

Step 1: Project Setup and Configuration

Set up a production-ready React TypeScript project:

# Create React app with TypeScript template
npx create-react-app react-typescript-app --template typescript
cd react-typescript-app

# Install additional dependencies
npm install @types/react @types/react-dom
npm install react-router-dom @types/react-router-dom
npm install @tanstack/react-query axios
npm install react-hook-form @hookform/resolvers zod
npm install @headlessui/react @heroicons/react
npm install clsx tailwind-merge

# Install development dependencies
npm install -D @typescript-eslint/eslint-plugin @typescript-eslint/parser
npm install -D prettier eslint-config-prettier
npm install -D @testing-library/react @testing-library/jest-dom
npm install -D @storybook/react @storybook/addon-essentials

Configure TypeScript for strict type checking:

{
  "compilerOptions": {
    "target": "ES2020",
    "lib": ["dom", "dom.iterable", "es6"],
    "allowJs": true,
    "skipLibCheck": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "strict": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true,
    "noImplicitThis": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "noUncheckedIndexedAccess": true,
    "noImplicitOverride": true,
    "exactOptionalPropertyTypes": true,
    "forceConsistentCasingInFileNames": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react-jsx",
    "baseUrl": "src",
    "paths": {
      "@/*": ["*"],
      "@/components/*": ["components/*"],
      "@/hooks/*": ["hooks/*"],
      "@/utils/*": ["utils/*"],
      "@/types/*": ["types/*"],
      "@/services/*": ["services/*"]
    }
  },
  "include": ["src"]
}tsconfig.json

Step 2: Advanced Type Definitions

Create comprehensive type definitions:

// Base types
export interface BaseEntity {
  id: string;
  createdAt: string;
  updatedAt: string;
}

// User types
export interface User extends BaseEntity {
  email: string;
  username: string;
  firstName: string;
  lastName: string;
  avatar?: string;
  role: UserRole;
  isActive: boolean;
  preferences: UserPreferences;
}

export type UserRole = "admin" | "user" | "moderator";

export interface UserPreferences {
  theme: "light" | "dark" | "system";
  language: string;
  notifications: {
    email: boolean;
    push: boolean;
    sms: boolean;
  };
  privacy: {
    profileVisibility: "public" | "friends" | "private";
    showEmail: boolean;
    showPhone: boolean;
  };
}

// API response types
export interface ApiResponse<T = any> {
  success: boolean;
  data?: T;
  message?: string;
  errors?: Record<string, string[]>;
  meta?: {
    pagination?: {
      page: number;
      limit: number;
      total: number;
      totalPages: number;
    };
    timestamp: string;
    requestId: string;
  };
}

export interface PaginatedResponse<T> {
  items: T[];
  pagination: {
    page: number;
    limit: number;
    total: number;
    totalPages: number;
    hasNext: boolean;
    hasPrev: boolean;
  };
}

// Form types
export interface FormField<T = any> {
  name: keyof T;
  label: string;
  type:
    | "text"
    | "email"
    | "password"
    | "number"
    | "select"
    | "textarea"
    | "checkbox"
    | "radio";
  placeholder?: string;
  required?: boolean;
  options?: Array<{ value: string; label: string }>;
  validation?: {
    min?: number;
    max?: number;
    pattern?: string;
    custom?: (value: any) => string | undefined;
  };
}

// Component props types
export interface BaseComponentProps {
  className?: string;
  children?: React.ReactNode;
  "data-testid"?: string;
}

export interface ButtonProps extends BaseComponentProps {
  variant?: "primary" | "secondary" | "danger" | "ghost";
  size?: "sm" | "md" | "lg";
  disabled?: boolean;
  loading?: boolean;
  onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
  type?: "button" | "submit" | "reset";
  icon?: React.ReactNode;
  iconPosition?: "left" | "right";
}

export interface InputProps extends BaseComponentProps {
  name: string;
  type?: "text" | "email" | "password" | "number" | "tel" | "url";
  placeholder?: string;
  value?: string;
  defaultValue?: string;
  disabled?: boolean;
  required?: boolean;
  error?: string;
  label?: string;
  helpText?: string;
  onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
  onBlur?: (event: React.FocusEvent<HTMLInputElement>) => void;
  onFocus?: (event: React.FocusEvent<HTMLInputElement>) => void;
}

// Hook types
export interface UseApiOptions<T> {
  initialData?: T;
  enabled?: boolean;
  onSuccess?: (data: T) => void;
  onError?: (error: Error) => void;
  retry?: number | boolean;
  retryDelay?: number;
  staleTime?: number;
  cacheTime?: number;
}

export interface UseFormOptions<T> {
  initialValues?: Partial<T>;
  validationSchema?: any;
  onSubmit: (values: T) => Promise<void> | void;
  validateOnBlur?: boolean;
  validateOnChange?: boolean;
}

// Utility types
export type Optional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
export type RequiredFields<T, K extends keyof T> = T & Required<Pick<T, K>>;

export type DeepPartial<T> = {
  [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};

export type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;

// Event handler types
export type EventHandler<T = Element> = (
  event: React.SyntheticEvent<T>
) => void;
export type ChangeHandler<T = HTMLInputElement> = (
  event: React.ChangeEvent<T>
) => void;
export type ClickHandler<T = HTMLButtonElement> = (
  event: React.MouseEvent<T>
) => void;
export type SubmitHandler<T = HTMLFormElement> = (
  event: React.FormEvent<T>
) => void;

// Generic component types
export type ComponentWithAs<P = {}> = <C extends React.ElementType = "div">(
  props: P & {
    as?: C;
  } & Omit<React.ComponentPropsWithoutRef<C>, keyof P | "as">
) => React.ReactElement | null;

export type PolymorphicRef<C extends React.ElementType> =
  React.ComponentPropsWithRef<C>["ref"];

export type PolymorphicComponentProp<
  C extends React.ElementType,
  Props = {},
> = React.PropsWithChildren<
  Props & {
    as?: C;
  }
> &
  Omit<React.ComponentPropsWithoutRef<C>, keyof Props>;

export type PolymorphicComponentPropWithRef<
  C extends React.ElementType,
  Props = {},
> = PolymorphicComponentProp<C, Props> & { ref?: PolymorphicRef<C> };src/types/index.ts

Step 3: Advanced Component Patterns

Create reusable components with advanced TypeScript patterns:

import React, { forwardRef } from "react";
import { clsx } from "clsx";
import { ButtonProps } from "@/types";

// Button variants configuration
const buttonVariants = {
  primary: "bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500",
  secondary: "bg-gray-600 text-white hover:bg-gray-700 focus:ring-gray-500",
  danger: "bg-red-600 text-white hover:bg-red-700 focus:ring-red-500",
  ghost: "bg-transparent text-gray-700 hover:bg-gray-100 focus:ring-gray-500",
} as const;

const buttonSizes = {
  sm: "px-3 py-1.5 text-sm",
  md: "px-4 py-2 text-base",
  lg: "px-6 py-3 text-lg",
} as const;

// Button component with forwarded ref
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
  (
    {
      variant = "primary",
      size = "md",
      disabled = false,
      loading = false,
      children,
      className,
      onClick,
      type = "button",
      icon,
      iconPosition = "left",
      "data-testid": testId,
      ...rest
    },
    ref
  ) => {
    const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
      if (disabled || loading) return;
      onClick?.(event);
    };

    const buttonClasses = clsx(
      // Base styles
      "inline-flex items-center justify-center font-medium rounded-md transition-colors duration-200",
      "focus:outline-none focus:ring-2 focus:ring-offset-2",
      "disabled:opacity-50 disabled:cursor-not-allowed",

      // Variant styles
      buttonVariants[variant],

      // Size styles
      buttonSizes[size],

      // Loading state
      loading && "cursor-wait",

      // Custom className
      className
    );

    const iconElement = icon && (
      <span
        className={clsx("flex-shrink-0", {
          "mr-2": iconPosition === "left" && children,
          "ml-2": iconPosition === "right" && children,
        })}
      >
        {icon}
      </span>
    );

    const loadingSpinner = loading && (
      <svg
        className={clsx("h-4 w-4 animate-spin", {
          "mr-2": children,
        })}
        fill="none"
        viewBox="0 0 24 24"
      >
        <circle
          cx="12"
          cy="12"
          r="10"
          stroke="currentColor"
          strokeWidth="4"
          className="opacity-25"
        />
        <path
          className="opacity-75"
          fill="currentColor"
          d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
        />
      </svg>
    );

    return (
      <button
        ref={ref}
        type={type}
        className={buttonClasses}
        disabled={disabled || loading}
        onClick={handleClick}
        data-testid={testId}
        aria-disabled={disabled || loading}
        aria-label={loading ? "Loading..." : undefined}
        {...rest}
      >
        {loading && loadingSpinner}
        {!loading && iconPosition === "left" && iconElement}
        {children}
        {!loading && iconPosition === "right" && iconElement}
      </button>
    );
  }
);

Button.displayName = "Button";

export default Button;src/components/Button/Button.tsx
import React from "react";
import {
  useController,
  Control,
  FieldPath,
  FieldValues,
} from "react-hook-form";
import { clsx } from "clsx";
import { InputProps } from "@/types";

// Generic form field component
interface FormFieldProps<
  TFieldValues extends FieldValues = FieldValues,
  TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
> extends Omit<InputProps, "name"> {
  name: TName;
  control: Control<TFieldValues>;
  rules?: {
    required?: boolean | string;
    minLength?: number | { value: number; message: string };
    maxLength?: number | { value: number; message: string };
    pattern?: { value: RegExp; message: string };
    validate?: (value: any) => string | boolean | undefined;
  };
}

export function FormField<
  TFieldValues extends FieldValues = FieldValues,
  TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
>({
  name,
  control,
  rules,
  label,
  helpText,
  className,
  type = "text",
  placeholder,
  disabled = false,
  required = false,
  "data-testid": testId,
  ...rest
}: FormFieldProps<TFieldValues, TName>) {
  const {
    field,
    fieldState: { error, invalid },
  } = useController({
    name,
    control,
    rules: {
      ...rules,
      required: required
        ? typeof rules?.required === "string"
          ? rules.required
          : `${label} is required`
        : false,
    },
  });

  const inputId = `field-${String(name)}`;
  const errorId = `${inputId}-error`;
  const helpId = `${inputId}-help`;

  const inputClasses = clsx(
    "block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm",
    "placeholder-gray-400 text-gray-900",
    "focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500",
    "disabled:bg-gray-50 disabled:text-gray-500 disabled:cursor-not-allowed",
    {
      "border-red-300 focus:ring-red-500 focus:border-red-500": invalid,
      "pr-10": invalid, // Space for error icon
    },
    className
  );

  return (
    <div className="space-y-1">
      {label && (
        <label
          htmlFor={inputId}
          className="block text-sm font-medium text-gray-700"
        >
          {label}
          {required && <span className="ml-1 text-red-500">*</span>}
        </label>
      )}

      <div className="relative">
        <input
          {...field}
          {...rest}
          id={inputId}
          type={type}
          placeholder={placeholder}
          disabled={disabled}
          className={inputClasses}
          aria-invalid={invalid}
          aria-describedby={clsx({
            [errorId]: invalid,
            [helpId]: helpText,
          })}
          data-testid={testId}
        />

        {invalid && (
          <div className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3">
            <svg
              className="h-5 w-5 text-red-500"
              fill="currentColor"
              viewBox="0 0 20 20"
              aria-hidden="true"
            >
              <path
                fillRule="evenodd"
                d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z"
                clipRule="evenodd"
              />
            </svg>
          </div>
        )}
      </div>

      {error && (
        <p id={errorId} className="text-sm text-red-600" role="alert">
          {error.message}
        </p>
      )}

      {helpText && !error && (
        <p id={helpId} className="text-sm text-gray-500">
          {helpText}
        </p>
      )}
    </div>
  );
}src/components/Form/FormField.tsx

Step 4: Advanced Custom Hooks

Create powerful custom hooks with TypeScript:

import { useState, useEffect, useCallback, useRef } from "react";
import { UseApiOptions } from "@/types";

interface UseApiState<T> {
  data: T | null;
  loading: boolean;
  error: Error | null;
}

interface UseApiReturn<T> extends UseApiState<T> {
  refetch: () => Promise<void>;
  mutate: (data: T) => void;
}

// Generic API hook with TypeScript
export function useApi<T = any>(
  fetcher: () => Promise<T>,
  options: UseApiOptions<T> = {}
): UseApiReturn<T> {
  const {
    initialData = null,
    enabled = true,
    onSuccess,
    onError,
    retry = 3,
    retryDelay = 1000,
    staleTime = 0,
    cacheTime = 5 * 60 * 1000, // 5 minutes
  } = options;

  const [state, setState] = useState<UseApiState<T>>({
    data: initialData,
    loading: false,
    error: null,
  });

  const retryCountRef = useRef(0);
  const fetchTimestampRef = useRef<number>(0);
  const cacheRef = useRef<{ data: T; timestamp: number } | null>(null);

  const isDataStale = useCallback(() => {
    if (!cacheRef.current) return true;
    return Date.now() - cacheRef.current.timestamp > staleTime;
  }, [staleTime]);

  const fetchData = useCallback(async (): Promise<void> => {
    // Return cached data if not stale
    if (cacheRef.current && !isDataStale()) {
      setState(prev => ({
        ...prev,
        data: cacheRef.current!.data,
        loading: false,
        error: null,
      }));
      return;
    }

    setState(prev => ({ ...prev, loading: true, error: null }));
    const fetchStart = Date.now();
    fetchTimestampRef.current = fetchStart;

    try {
      const result = await fetcher();

      // Ignore result if newer request exists
      if (fetchTimestampRef.current !== fetchStart) return;

      // Cache the result
      cacheRef.current = { data: result, timestamp: Date.now() };

      setState(prev => ({
        ...prev,
        data: result,
        loading: false,
        error: null,
      }));
      retryCountRef.current = 0;
      onSuccess?.(result);
    } catch (error) {
      // Ignore error if newer request exists
      if (fetchTimestampRef.current !== fetchStart) return;

      const apiError =
        error instanceof Error ? error : new Error("Unknown error");

      // Retry logic
      if (retryCountRef.current < (typeof retry === "number" ? retry : 3)) {
        retryCountRef.current += 1;
        setTimeout(() => {
          if (fetchTimestampRef.current === fetchStart) {
            fetchData();
          }
        }, retryDelay * retryCountRef.current);
        return;
      }

      setState(prev => ({ ...prev, loading: false, error: apiError }));
      onError?.(apiError);
    }
  }, [fetcher, onSuccess, onError, retry, retryDelay, isDataStale]);

  const refetch = useCallback(async (): Promise<void> => {
    // Clear cache to force fresh fetch
    cacheRef.current = null;
    await fetchData();
  }, [fetchData]);

  const mutate = useCallback((data: T): void => {
    // Update cache and state
    cacheRef.current = { data, timestamp: Date.now() };
    setState(prev => ({ ...prev, data, error: null }));
  }, []);

  useEffect(() => {
    if (enabled) {
      fetchData();
    }
  }, [enabled, fetchData]);

  // Cleanup cache when component unmounts
  useEffect(() => {
    return () => {
      const timeout = setTimeout(() => {
        cacheRef.current = null;
      }, cacheTime);

      return () => clearTimeout(timeout);
    };
  }, [cacheTime]);

  return {
    ...state,
    refetch,
    mutate,
  };
}src/hooks/useApi.ts
import { useState, useEffect, useCallback } from "react";

type SetValue<T> = T | ((prev: T) => T);

interface UseLocalStorageReturn<T> {
  value: T;
  setValue: (value: SetValue<T>) => void;
  remove: () => void;
}

// Type-safe local storage hook
export function useLocalStorage<T>(
  key: string,
  initialValue: T,
  options: {
    serialize?: (value: T) => string;
    deserialize?: (value: string) => T;
  } = {}
): UseLocalStorageReturn<T> {
  const { serialize = JSON.stringify, deserialize = JSON.parse } = options;

  // Get value from localStorage
  const getStoredValue = useCallback((): T => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? deserialize(item) : initialValue;
    } catch (error) {
      console.error(`Error reading localStorage key "${key}":`, error);
      return initialValue;
    }
  }, [key, initialValue, deserialize]);

  const [value, setValue] = useState<T>(getStoredValue);

  // Set value in localStorage
  const setStoredValue = useCallback(
    (newValue: SetValue<T>) => {
      try {
        const valueToStore =
          typeof newValue === "function"
            ? (newValue as (prev: T) => T)(value)
            : newValue;

        setValue(valueToStore);

        if (valueToStore === undefined) {
          window.localStorage.removeItem(key);
        } else {
          window.localStorage.setItem(key, serialize(valueToStore));
        }
      } catch (error) {
        console.error(`Error setting localStorage key "${key}":`, error);
      }
    },
    [key, serialize, value]
  );

  // Remove value from localStorage
  const removeValue = useCallback(() => {
    try {
      window.localStorage.removeItem(key);
      setValue(initialValue);
    } catch (error) {
      console.error(`Error removing localStorage key "${key}":`, error);
    }
  }, [key, initialValue]);

  // Listen for changes in localStorage
  useEffect(() => {
    const handleStorageChange = (e: StorageEvent) => {
      if (e.key === key && e.newValue !== serialize(value)) {
        setValue(e.newValue ? deserialize(e.newValue) : initialValue);
      }
    };

    window.addEventListener("storage", handleStorageChange);
    return () => window.removeEventListener("storage", handleStorageChange);
  }, [key, value, initialValue, serialize, deserialize]);

  return {
    value,
    setValue: setStoredValue,
    remove: removeValue,
  };
}src/hooks/useLocalStorage.ts
import { useState, useEffect } from "react";

// Debounce hook with TypeScript
export function useDebounce<T>(value: T, delay: number): T {
  const [debouncedValue, setDebouncedValue] = useState<T>(value);

  useEffect(() => {
    const handler = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);

    return () => {
      clearTimeout(handler);
    };
  }, [value, delay]);

  return debouncedValue;
}

// Debounced callback hook
export function useDebouncedCallback<T extends (...args: any[]) => any>(
  callback: T,
  delay: number,
  deps: React.DependencyList = []
): T {
  const [debouncedCallback, setDebouncedCallback] = useState<T>(callback);

  useEffect(() => {
    const handler = setTimeout(() => {
      setDebouncedCallback(() => callback);
    }, delay);

    return () => {
      clearTimeout(handler);
    };
  }, [callback, delay, ...deps]);

  return debouncedCallback;
}src/hooks/useDebounce.ts

Step 5: Performance Optimization Components

Create performance-optimized components:

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

interface VirtualListProps<T> {
  items: T[];
  itemHeight: number;
  containerHeight: number;
  renderItem: (item: T, index: number) => React.ReactNode;
  overscan?: number;
  className?: string;
  onScroll?: (scrollTop: number) => void;
}

// High-performance virtual list component
export function VirtualList<T>({
  items,
  itemHeight,
  containerHeight,
  renderItem,
  overscan = 5,
  className,
  onScroll,
}: VirtualListProps<T>) {
  const [scrollTop, setScrollTop] = useState(0);
  const scrollElementRef = useRef<HTMLDivElement>(null);

  // Calculate visible range
  const { startIndex, endIndex, totalHeight } = useMemo(() => {
    const start = Math.floor(scrollTop / itemHeight);
    const visibleCount = Math.ceil(containerHeight / itemHeight);
    const end = Math.min(items.length - 1, start + visibleCount);

    return {
      startIndex: Math.max(0, start - overscan),
      endIndex: Math.min(items.length - 1, end + overscan),
      totalHeight: items.length * itemHeight,
    };
  }, [scrollTop, itemHeight, containerHeight, items.length, overscan]);

  // Get visible items
  const visibleItems = useMemo(() => {
    return items.slice(startIndex, endIndex + 1).map((item, index) => ({
      item,
      index: startIndex + index,
    }));
  }, [items, startIndex, endIndex]);

  // Handle scroll
  const handleScroll = useCallback(
    (event: React.UIEvent<HTMLDivElement>) => {
      const newScrollTop = event.currentTarget.scrollTop;
      setScrollTop(newScrollTop);
      onScroll?.(newScrollTop);
    },
    [onScroll]
  );

  // Scroll to index
  const scrollToIndex = useCallback(
    (index: number) => {
      if (scrollElementRef.current) {
        const scrollTop = index * itemHeight;
        scrollElementRef.current.scrollTop = scrollTop;
        setScrollTop(scrollTop);
      }
    },
    [itemHeight]
  );

  return (
    <div
      ref={scrollElementRef}
      className={className}
      style={{ height: containerHeight, overflow: "auto" }}
      onScroll={handleScroll}
    >
      <div style={{ height: totalHeight, position: "relative" }}>
        <div
          style={{
            transform: `translateY(${startIndex * itemHeight}px)`,
          }}
        >
          {visibleItems.map(({ item, index }) => (
            <div
              key={index}
              style={{
                height: itemHeight,
                display: "flex",
                alignItems: "center",
              }}
            >
              {renderItem(item, index)}
            </div>
          ))}
        </div>
      </div>
    </div>
  );
}src/components/VirtualList/VirtualList.tsx
import React, { useState, useRef, useEffect, useCallback } from "react";
import { clsx } from "clsx";

interface LazyImageProps extends React.ImgHTMLAttributes<HTMLImageElement> {
  src: string;
  alt: string;
  placeholder?: string;
  fallback?: string;
  threshold?: number;
  rootMargin?: string;
  className?: string;
  onLoad?: () => void;
  onError?: () => void;
}

// Lazy loading image component with intersection observer
export function LazyImage({
  src,
  alt,
  placeholder,
  fallback,
  threshold = 0.1,
  rootMargin = "50px",
  className,
  onLoad,
  onError,
  ...props
}: LazyImageProps) {
  const [imageSrc, setImageSrc] = useState<string | undefined>(placeholder);
  const [isLoading, setIsLoading] = useState(true);
  const [hasError, setHasError] = useState(false);
  const [isInView, setIsInView] = useState(false);
  const imgRef = useRef<HTMLImageElement>(null);

  // Intersection Observer
  useEffect(() => {
    const observer = new IntersectionObserver(
      ([entry]) => {
        if (entry.isIntersecting) {
          setIsInView(true);
          observer.disconnect();
        }
      },
      { threshold, rootMargin }
    );

    const currentImg = imgRef.current;
    if (currentImg) {
      observer.observe(currentImg);
    }

    return () => {
      if (currentImg) {
        observer.unobserve(currentImg);
      }
    };
  }, [threshold, rootMargin]);

  // Load image when in view
  useEffect(() => {
    if (isInView && src) {
      const imageLoader = new Image();

      imageLoader.onload = () => {
        setImageSrc(src);
        setIsLoading(false);
        setHasError(false);
        onLoad?.();
      };

      imageLoader.onerror = () => {
        setHasError(true);
        setIsLoading(false);
        if (fallback) {
          setImageSrc(fallback);
        }
        onError?.();
      };

      imageLoader.src = src;
    }
  }, [isInView, src, fallback, onLoad, onError]);

  const imageClasses = clsx(
    "transition-opacity duration-300",
    {
      "opacity-0": isLoading,
      "opacity-100": !isLoading,
    },
    className
  );

  return (
    <div className="relative overflow-hidden">
      <img
        ref={imgRef}
        src={imageSrc}
        alt={alt}
        className={imageClasses}
        {...props}
      />

      {isLoading && (
        <div className="absolute inset-0 flex animate-pulse items-center justify-center bg-gray-200">
          <svg
            className="h-6 w-6 text-gray-400"
            fill="none"
            stroke="currentColor"
            viewBox="0 0 24 24"
          >
            <path
              strokeLinecap="round"
              strokeLinejoin="round"
              strokeWidth={2}
              d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"
            />
          </svg>
        </div>
      )}

      {hasError && !fallback && (
        <div className="absolute inset-0 flex items-center justify-center bg-gray-100">
          <div className="text-center text-gray-500">
            <svg
              className="mx-auto mb-2 h-8 w-8"
              fill="none"
              stroke="currentColor"
              viewBox="0 0 24 24"
            >
              <path
                strokeLinecap="round"
                strokeLinejoin="round"
                strokeWidth={2}
                d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
              />
            </svg>
            <p className="text-sm">Failed to load image</p>
          </div>
        </div>
      )}
    </div>
  );
}src/components/LazyImage/LazyImage.tsx

Step 6: Advanced Error Boundary

Create a comprehensive error boundary with TypeScript:

import React, { Component, ErrorInfo, ReactNode } from "react";

interface ErrorBoundaryProps {
  children: ReactNode;
  fallback?: (error: Error, errorInfo: ErrorInfo) => ReactNode;
  onError?: (error: Error, errorInfo: ErrorInfo) => void;
  resetKeys?: Array<string | number>;
  resetOnPropsChange?: boolean;
}

interface ErrorBoundaryState {
  hasError: boolean;
  error: Error | null;
  errorInfo: ErrorInfo | null;
}

export class ErrorBoundary extends Component<
  ErrorBoundaryProps,
  ErrorBoundaryState
> {
  constructor(props: ErrorBoundaryProps) {
    super(props);
    this.state = {
      hasError: false,
      error: null,
      errorInfo: null,
    };
  }

  static getDerivedStateFromError(error: Error): Partial<ErrorBoundaryState> {
    return {
      hasError: true,
      error,
    };
  }

  componentDidCatch(error: Error, errorInfo: ErrorInfo) {
    this.setState({
      error,
      errorInfo,
    });

    // Log error to external service
    this.props.onError?.(error, errorInfo);

    // Log to console in development
    if (process.env.NODE_ENV === "development") {
      console.error("ErrorBoundary caught an error:", error, errorInfo);
    }
  }

  componentDidUpdate(prevProps: ErrorBoundaryProps) {
    const { resetKeys, resetOnPropsChange } = this.props;
    const { hasError } = this.state;

    if (
      hasError &&
      resetOnPropsChange &&
      prevProps.children !== this.props.children
    ) {
      this.resetErrorBoundary();
    }

    if (hasError && resetKeys) {
      const hasResetKeyChanged = resetKeys.some(
        (key, idx) => prevProps.resetKeys?.[idx] !== key
      );

      if (hasResetKeyChanged) {
        this.resetErrorBoundary();
      }
    }
  }

  resetErrorBoundary = () => {
    this.setState({
      hasError: false,
      error: null,
      errorInfo: null,
    });
  };

  render() {
    const { hasError, error, errorInfo } = this.state;
    const { fallback, children } = this.props;

    if (hasError && error) {
      if (fallback) {
        return fallback(error, errorInfo!);
      }

      return (
        <DefaultErrorFallback
          error={error}
          resetError={this.resetErrorBoundary}
        />
      );
    }

    return children;
  }
}

// Default error fallback component
interface DefaultErrorFallbackProps {
  error: Error;
  resetError: () => void;
}

function DefaultErrorFallback({
  error,
  resetError,
}: DefaultErrorFallbackProps) {
  return (
    <div className="flex min-h-screen flex-col justify-center bg-gray-50 py-12 sm:px-6 lg:px-8">
      <div className="sm:mx-auto sm:w-full sm:max-w-md">
        <div className="bg-white px-4 py-8 shadow sm:rounded-lg sm:px-10">
          <div className="text-center">
            <svg
              className="mx-auto h-12 w-12 text-red-400"
              fill="none"
              viewBox="0 0 24 24"
              stroke="currentColor"
              aria-hidden="true"
            >
              <path
                vectorEffect="non-scaling-stroke"
                strokeLinecap="round"
                strokeLinejoin="round"
                strokeWidth={2}
                d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.268 16.5c-.77.833.192 2.5 1.732 2.5z"
              />
            </svg>
            <h2 className="mt-4 text-lg font-medium text-gray-900">
              Something went wrong
            </h2>
            <p className="mt-2 text-sm text-gray-600">
              We're sorry, but something unexpected happened. Please try again.
            </p>
            {process.env.NODE_ENV === "development" && (
              <details className="mt-4 text-left">
                <summary className="cursor-pointer text-sm text-gray-500 hover:text-gray-700">
                  Error details (development only)
                </summary>
                <pre className="mt-2 whitespace-pre-wrap text-xs text-red-600">
                  {error.message}
                  {error.stack}
                </pre>
              </details>
            )}
            <div className="mt-6">
              <button
                type="button"
                onClick={resetError}
                className="flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
              >
                Try again
              </button>
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}

// Hook for error boundary
export function useErrorHandler() {
  const [error, setError] = React.useState<Error | null>(null);

  const resetError = React.useCallback(() => {
    setError(null);
  }, []);

  const captureError = React.useCallback((error: Error) => {
    setError(error);
  }, []);

  React.useEffect(() => {
    if (error) {
      throw error;
    }
  }, [error]);

  return { captureError, resetError };
}src/components/ErrorBoundary/ErrorBoundary.tsx

Best Practices Summary

  1. Use strict TypeScript configuration for maximum type safety
  2. Implement proper component patterns with forwarded refs and generic types
  3. Create reusable custom hooks with proper TypeScript generics
  4. Optimize performance with virtual lists and lazy loading
  5. Handle errors gracefully with comprehensive error boundaries
  6. Use proper form handling with type-safe validation
  7. Implement accessibility features in all components
  8. Write comprehensive tests for components and hooks
  9. Document component APIs with proper TypeScript interfaces
  10. Follow React best practices for state management and effects

Development Commands

# Start development server
npm start

# Run type checking
npm run type-check

# Build for production
npm run build

# Run tests
npm test

# Run Storybook
npm run storybook

Your React TypeScript application is now ready for production with advanced component patterns, type safety, performance optimizations, and comprehensive error handling!


Share this post on:

Previous Post
Expo React Native TypeScript: Cross-Platform Mobile Development
Next Post
Prisma TypeScript: Modern Database ORM Development