+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Part 156 of 354

๐Ÿ“˜ React Class Components: Legacy Type Safety

Master react class components: legacy type safety in TypeScript with practical examples, best practices, and real-world applications ๐Ÿš€

๐Ÿš€Intermediate
25 min read

Prerequisites

  • Basic understanding of JavaScript ๐Ÿ“
  • TypeScript installation โšก
  • VS Code or preferred IDE ๐Ÿ’ป

What you'll learn

  • Understand the concept fundamentals ๐ŸŽฏ
  • Apply the concept in real projects ๐Ÿ—๏ธ
  • Debug common issues ๐Ÿ›
  • Write type-safe code โœจ

๐ŸŽฏ Introduction

Welcome to the world of React Class Components with TypeScript! ๐ŸŽ‰ While functional components have taken the spotlight in recent years, understanding class components is still crucial for maintaining legacy code and working with older React applications.

Youโ€™ll discover how TypeScript transforms class components from error-prone JavaScript into bulletproof, type-safe building blocks ๐Ÿ›ก๏ธ. Whether youโ€™re maintaining existing applications ๐Ÿ”ง, working with third-party libraries ๐Ÿ“š, or simply want to understand Reactโ€™s evolution, mastering class component typing is essential!

By the end of this tutorial, youโ€™ll feel confident typing class components like a pro! Letโ€™s dive into the legacy magic! ๐ŸŠโ€โ™‚๏ธ

๐Ÿ“š Understanding React Class Components

๐Ÿค” What are React Class Components?

React Class Components are like traditional JavaScript classes that extend React.Component ๐Ÿ—๏ธ. Think of them as blueprints for creating reusable UI components that can hold state and respond to lifecycle events.

In TypeScript terms, class components provide excellent type safety for:

  • โœจ Props validation and autocomplete
  • ๐Ÿš€ State management with strict types
  • ๐Ÿ›ก๏ธ Method signatures and return types
  • ๐Ÿ“– Clear component contracts

๐Ÿ’ก Why Use TypeScript with Class Components?

Hereโ€™s why developers love TypeScript with class components:

  1. Type Safety ๐Ÿ”’: Catch prop and state errors at compile-time
  2. Better IDE Support ๐Ÿ’ป: Amazing autocomplete and refactoring
  3. Legacy Code Maintenance ๐Ÿ“–: Easier to understand and modify old code
  4. Team Collaboration ๐Ÿ”ง: Clear contracts between team members

Real-world example: Imagine maintaining a large e-commerce dashboard ๐Ÿ›’. With TypeScript, you can safely refactor product listing components without breaking anything!

๐Ÿ”ง Basic Syntax and Usage

๐Ÿ“ Simple Class Component

Letโ€™s start with a friendly example:

import React, { Component } from 'react';

// ๐ŸŽฏ Define our props interface
interface GreetingProps {
  name: string;        // ๐Ÿ‘ค User's name
  age?: number;        // ๐ŸŽ‚ Optional age
  emoji?: string;      // ๐Ÿ˜Š Fun emoji
}

// ๐ŸŽจ Define our state interface
interface GreetingState {
  isVisible: boolean;  // ๐Ÿ‘๏ธ Visibility toggle
  clickCount: number;  // ๐Ÿ”ข Track clicks
}

// ๐Ÿ—๏ธ Our typed class component
class Greeting extends Component<GreetingProps, GreetingState> {
  // ๐ŸŽฌ Initialize state
  state: GreetingState = {
    isVisible: true,
    clickCount: 0
  };

  // ๐ŸŽฏ Handle button click
  handleClick = (): void => {
    this.setState(prevState => ({
      clickCount: prevState.clickCount + 1,
      isVisible: !prevState.isVisible
    }));
  };

  // ๐ŸŽจ Render method
  render(): JSX.Element {
    const { name, age, emoji = '๐Ÿ‘‹' } = this.props;
    const { isVisible, clickCount } = this.state;

    return (
      <div className="greeting-card">
        {isVisible && (
          <h1>{emoji} Hello, {name}!</h1>
        )}
        {age && <p>๐ŸŽ‚ Age: {age}</p>}
        <button onClick={this.handleClick}>
          {isVisible ? 'Hide' : 'Show'} Greeting
        </button>
        <p>๐Ÿ”ข Clicks: {clickCount}</p>
      </div>
    );
  }
}

๐Ÿ’ก Explanation: Notice how we define separate interfaces for props and state, then pass them as type parameters to Component<Props, State>!

๐ŸŽฏ Common Patterns

Here are patterns youโ€™ll use daily:

// ๐Ÿ—๏ธ Pattern 1: Component with default props
interface ButtonProps {
  text: string;
  variant?: 'primary' | 'secondary';
  onClick: () => void;
  disabled?: boolean;
}

class Button extends Component<ButtonProps> {
  // ๐ŸŽจ Default props (typed!)
  static defaultProps: Partial<ButtonProps> = {
    variant: 'primary',
    disabled: false
  };

  render(): JSX.Element {
    const { text, variant, onClick, disabled } = this.props;
    
    return (
      <button 
        className={`btn btn-${variant}`}
        onClick={onClick}
        disabled={disabled}
      >
        {text}
      </button>
    );
  }
}

// ๐Ÿ”„ Pattern 2: Event handlers with proper typing
interface FormProps {
  onSubmit: (data: FormData) => void;
}

interface FormData {
  username: string;
  email: string;
}

class LoginForm extends Component<FormProps, FormData> {
  state: FormData = {
    username: '',
    email: ''
  };

  // ๐ŸŽฏ Typed event handler
  handleInputChange = (event: React.ChangeEvent<HTMLInputElement>): void => {
    const { name, value } = event.target;
    this.setState({ [name]: value } as Pick<FormData, keyof FormData>);
  };

  // ๐Ÿ“ค Form submission
  handleSubmit = (event: React.FormEvent<HTMLFormElement>): void => {
    event.preventDefault();
    this.props.onSubmit(this.state);
  };
}

๐Ÿ’ก Practical Examples

๐Ÿ›’ Example 1: Shopping Cart Component

Letโ€™s build something real:

// ๐Ÿ›๏ธ Define our product and cart interfaces
interface Product {
  id: string;
  name: string;
  price: number;
  image: string;
  emoji: string; // Every product needs an emoji! 
}

interface CartItem extends Product {
  quantity: number;
}

interface CartProps {
  products: Product[];
  onCheckout: (items: CartItem[]) => void;
}

interface CartState {
  items: CartItem[];
  isOpen: boolean;
  total: number;
}

// ๐Ÿ›’ Shopping cart class component
class ShoppingCart extends Component<CartProps, CartState> {
  state: CartState = {
    items: [],
    isOpen: false,
    total: 0
  };

  // โž• Add item to cart
  addToCart = (product: Product): void => {
    this.setState(prevState => {
      const existingItem = prevState.items.find(item => item.id === product.id);
      
      let updatedItems: CartItem[];
      if (existingItem) {
        // ๐Ÿ”„ Update quantity if item exists
        updatedItems = prevState.items.map(item =>
          item.id === product.id 
            ? { ...item, quantity: item.quantity + 1 }
            : item
        );
      } else {
        // โœจ Add new item
        updatedItems = [...prevState.items, { ...product, quantity: 1 }];
      }

      // ๐Ÿ’ฐ Calculate new total
      const total = updatedItems.reduce((sum, item) => 
        sum + (item.price * item.quantity), 0
      );

      console.log(`๐Ÿ›’ Added ${product.emoji} ${product.name} to cart!`);
      
      return { items: updatedItems, total };
    });
  };

  // ๐Ÿ—‘๏ธ Remove item from cart
  removeFromCart = (productId: string): void => {
    this.setState(prevState => {
      const updatedItems = prevState.items.filter(item => item.id !== productId);
      const total = updatedItems.reduce((sum, item) => 
        sum + (item.price * item.quantity), 0
      );
      
      return { items: updatedItems, total };
    });
  };

  // ๐ŸŽฏ Toggle cart visibility
  toggleCart = (): void => {
    this.setState(prevState => ({ isOpen: !prevState.isOpen }));
  };

  // ๐Ÿ’ณ Handle checkout
  handleCheckout = (): void => {
    this.props.onCheckout(this.state.items);
    this.setState({ items: [], total: 0, isOpen: false });
  };

  // ๐ŸŽจ Render cart items
  renderCartItems(): JSX.Element[] {
    return this.state.items.map(item => (
      <div key={item.id} className="cart-item">
        <span>{item.emoji} {item.name}</span>
        <span>Qty: {item.quantity}</span>
        <span>${(item.price * item.quantity).toFixed(2)}</span>
        <button onClick={() => this.removeFromCart(item.id)}>
          ๐Ÿ—‘๏ธ Remove
        </button>
      </div>
    ));
  }

  // ๐Ÿ—๏ธ Main render method
  render(): JSX.Element {
    const { products } = this.props;
    const { isOpen, items, total } = this.state;

    return (
      <div className="shopping-cart">
        <button onClick={this.toggleCart} className="cart-toggle">
          ๐Ÿ›’ Cart ({items.length}) - ${total.toFixed(2)}
        </button>

        {isOpen && (
          <div className="cart-dropdown">
            <h3>๐Ÿ›๏ธ Your Cart</h3>
            {items.length === 0 ? (
              <p>๐Ÿ˜” Your cart is empty</p>
            ) : (
              <>
                {this.renderCartItems()}
                <div className="cart-total">
                  <strong>๐Ÿ’ฐ Total: ${total.toFixed(2)}</strong>
                </div>
                <button onClick={this.handleCheckout} className="checkout-btn">
                  ๐Ÿ’ณ Checkout
                </button>
              </>
            )}
          </div>
        )}

        <div className="products">
          <h2>๐Ÿช Products</h2>
          {products.map(product => (
            <div key={product.id} className="product-card">
              <h3>{product.emoji} {product.name}</h3>
              <p>${product.price}</p>
              <button onClick={() => this.addToCart(product)}>
                โž• Add to Cart
              </button>
            </div>
          ))}
        </div>
      </div>
    );
  }
}

๐ŸŽฏ Try it yourself: Add quantity controls and a wishlist feature!

๐ŸŽฎ Example 2: Game Score Dashboard

Letโ€™s make it fun:

// ๐Ÿ† Game interfaces
interface Player {
  id: string;
  name: string;
  avatar: string;
  level: number;
}

interface GameScore {
  playerId: string;
  score: number;
  timestamp: Date;
  achievements: string[];
}

interface LeaderboardProps {
  players: Player[];
  maxScores?: number;
}

interface LeaderboardState {
  scores: GameScore[];
  selectedPlayer: string | null;
  sortBy: 'score' | 'timestamp';
  isLoading: boolean;
}

// ๐ŸŽฎ Game leaderboard component
class GameLeaderboard extends Component<LeaderboardProps, LeaderboardState> {
  // ๐ŸŽจ Default props
  static defaultProps: Partial<LeaderboardProps> = {
    maxScores: 10
  };

  state: LeaderboardState = {
    scores: [],
    selectedPlayer: null,
    sortBy: 'score',
    isLoading: false
  };

  // ๐ŸŽฌ Component lifecycle
  componentDidMount(): void {
    this.loadScores();
  }

  // ๐Ÿ“Š Load scores (simulated API call)
  loadScores = async (): Promise<void> => {
    this.setState({ isLoading: true });
    
    // ๐ŸŽฏ Simulate API delay
    await new Promise(resolve => setTimeout(resolve, 1000));
    
    const mockScores: GameScore[] = this.props.players.map(player => ({
      playerId: player.id,
      score: Math.floor(Math.random() * 10000),
      timestamp: new Date(),
      achievements: ['๐ŸŒŸ First Steps', '๐Ÿ† High Scorer']
    }));

    this.setState({ scores: mockScores, isLoading: false });
  };

  // ๐Ÿ”„ Sort scores
  sortScores = (scores: GameScore[]): GameScore[] => {
    return [...scores].sort((a, b) => {
      if (this.state.sortBy === 'score') {
        return b.score - a.score; // ๐Ÿ“ˆ Highest first
      }
      return b.timestamp.getTime() - a.timestamp.getTime(); // ๐Ÿ• Latest first
    });
  };

  // ๐ŸŽฏ Handle player selection
  selectPlayer = (playerId: string): void => {
    this.setState(prevState => ({
      selectedPlayer: prevState.selectedPlayer === playerId ? null : playerId
    }));
  };

  // ๐Ÿ“Š Get player details
  getPlayerDetails = (playerId: string): Player | undefined => {
    return this.props.players.find(player => player.id === playerId);
  };

  // ๐ŸŽจ Render player score
  renderPlayerScore = (score: GameScore, index: number): JSX.Element => {
    const player = this.getPlayerDetails(score.playerId);
    if (!player) return <div key={score.playerId}>โŒ Player not found</div>;

    const isSelected = this.state.selectedPlayer === score.playerId;
    const medalEmoji = index === 0 ? '๐Ÿฅ‡' : index === 1 ? '๐Ÿฅˆ' : index === 2 ? '๐Ÿฅ‰' : '๐Ÿ…';

    return (
      <div 
        key={score.playerId}
        className={`score-row ${isSelected ? 'selected' : ''}`}
        onClick={() => this.selectPlayer(score.playerId)}
      >
        <div className="rank">{medalEmoji} #{index + 1}</div>
        <div className="player-info">
          <img src={player.avatar} alt={player.name} className="avatar" />
          <span>{player.name}</span>
        </div>
        <div className="score">๐ŸŽฏ {score.score.toLocaleString()}</div>
        <div className="level">๐ŸŽŠ Level {player.level}</div>
        
        {isSelected && (
          <div className="achievements">
            <h4>๐Ÿ† Achievements:</h4>
            {score.achievements.map((achievement, i) => (
              <span key={i} className="achievement">{achievement}</span>
            ))}
          </div>
        )}
      </div>
    );
  };

  // ๐Ÿ—๏ธ Main render
  render(): JSX.Element {
    const { maxScores } = this.props;
    const { scores, sortBy, isLoading } = this.state;

    if (isLoading) {
      return <div className="loading">โณ Loading leaderboard...</div>;
    }

    const sortedScores = this.sortScores(scores).slice(0, maxScores);

    return (
      <div className="game-leaderboard">
        <h1>๐Ÿ† Game Leaderboard</h1>
        
        <div className="controls">
          <button 
            onClick={() => this.setState({ sortBy: 'score' })}
            className={sortBy === 'score' ? 'active' : ''}
          >
            ๐Ÿ“Š Sort by Score
          </button>
          <button 
            onClick={() => this.setState({ sortBy: 'timestamp' })}
            className={sortBy === 'timestamp' ? 'active' : ''}
          >
            ๐Ÿ• Sort by Time
          </button>
          <button onClick={this.loadScores}>
            ๐Ÿ”„ Refresh
          </button>
        </div>

        <div className="scores-list">
          {sortedScores.map((score, index) => 
            this.renderPlayerScore(score, index)
          )}
        </div>

        {scores.length === 0 && (
          <div className="empty-state">
            ๐ŸŽฎ No scores yet! Start playing to see the leaderboard!
          </div>
        )}
      </div>
    );
  }
}

๐Ÿš€ Advanced Concepts

๐Ÿง™โ€โ™‚๏ธ Advanced Topic 1: Higher-Order Components (HOCs)

When youโ€™re ready to level up, try this advanced pattern:

// ๐ŸŽฏ HOC for adding loading state
interface WithLoadingProps {
  isLoading: boolean;
}

function withLoading<P extends object>(
  WrappedComponent: React.ComponentType<P>
): React.ComponentType<P & WithLoadingProps> {
  
  return class WithLoadingComponent extends Component<P & WithLoadingProps> {
    render(): JSX.Element {
      const { isLoading, ...otherProps } = this.props;
      
      if (isLoading) {
        return <div className="loading">โณ Loading...</div>;
      }
      
      return <WrappedComponent {...otherProps as P} />;
    }
  };
}

// ๐Ÿช„ Using the HOC
interface UserListProps {
  users: User[];
}

class UserList extends Component<UserListProps> {
  render(): JSX.Element {
    return (
      <div>
        {this.props.users.map(user => (
          <div key={user.id}>๐Ÿ‘ค {user.name}</div>
        ))}
      </div>
    );
  }
}

// โœจ Enhanced component with loading
const UserListWithLoading = withLoading(UserList);

๐Ÿ—๏ธ Advanced Topic 2: Render Props Pattern

For the brave developers:

// ๐Ÿš€ Render props for data fetching
interface DataFetcherProps<T> {
  url: string;
  children: (data: T | null, loading: boolean, error: string | null) => JSX.Element;
}

interface DataFetcherState<T> {
  data: T | null;
  loading: boolean;
  error: string | null;
}

class DataFetcher<T> extends Component<DataFetcherProps<T>, DataFetcherState<T>> {
  state: DataFetcherState<T> = {
    data: null,
    loading: false,
    error: null
  };

  async componentDidMount(): Promise<void> {
    this.fetchData();
  }

  fetchData = async (): Promise<void> => {
    this.setState({ loading: true, error: null });
    
    try {
      const response = await fetch(this.props.url);
      const data = await response.json();
      this.setState({ data, loading: false });
    } catch (error) {
      this.setState({ 
        error: error instanceof Error ? error.message : 'Unknown error',
        loading: false 
      });
    }
  };

  render(): JSX.Element {
    const { data, loading, error } = this.state;
    return this.props.children(data, loading, error);
  }
}

// ๐ŸŽฏ Usage with type safety
interface User {
  id: number;
  name: string;
  email: string;
}

const UserProfile: React.FC = () => (
  <DataFetcher<User[]> url="/api/users">
    {(users, loading, error) => {
      if (loading) return <div>โณ Loading users...</div>;
      if (error) return <div>โŒ Error: {error}</div>;
      if (!users) return <div>๐Ÿคทโ€โ™‚๏ธ No users found</div>;
      
      return (
        <div>
          {users.map(user => (
            <div key={user.id}>๐Ÿ‘ค {user.name} - {user.email}</div>
          ))}
        </div>
      );
    }}
  </DataFetcher>
);

โš ๏ธ Common Pitfalls and Solutions

๐Ÿ˜ฑ Pitfall 1: Incorrect State Updates

// โŒ Wrong way - mutating state directly!
class BadCounter extends Component<{}, { count: number }> {
  state = { count: 0 };

  increment = (): void => {
    this.state.count++; // ๐Ÿ’ฅ Never mutate state directly!
    this.forceUpdate(); // ๐Ÿ˜ฐ This is a code smell!
  };
}

// โœ… Correct way - use setState!
class GoodCounter extends Component<{}, { count: number }> {
  state = { count: 0 };

  increment = (): void => {
    this.setState(prevState => ({
      count: prevState.count + 1 // โœจ Immutable update
    }));
  };
}

๐Ÿคฏ Pitfall 2: Not Binding Event Handlers

// โŒ Dangerous - 'this' context will be lost!
class BadButton extends Component<{ onClick: () => void }> {
  handleClick() { // ๐Ÿ’ฅ Not bound properly!
    console.log(this); // undefined in strict mode!
    this.props.onClick(); // ๐Ÿ’ฅ Error!
  }

  render(): JSX.Element {
    return <button onClick={this.handleClick}>Click me</button>;
  }
}

// โœ… Safe - proper binding methods!
class GoodButton extends Component<{ onClick: () => void }> {
  // ๐ŸŽฏ Method 1: Arrow function (recommended)
  handleClick = (): void => {
    console.log(this); // โœ… Correctly bound!
    this.props.onClick();
  };

  // ๐ŸŽฏ Method 2: Bind in constructor
  constructor(props: { onClick: () => void }) {
    super(props);
    this.handleClick2 = this.handleClick2.bind(this);
  }

  handleClick2(): void {
    this.props.onClick();
  }

  render(): JSX.Element {
    return (
      <div>
        <button onClick={this.handleClick}>โœ… Arrow Function</button>
        <button onClick={this.handleClick2}>โœ… Bound in Constructor</button>
      </div>
    );
  }
}

๐Ÿ”„ Pitfall 3: Memory Leaks with Timers

// โŒ Memory leak - timer not cleaned up!
class BadTimer extends Component<{}, { time: number }> {
  state = { time: 0 };

  componentDidMount(): void {
    setInterval(() => {
      this.setState({ time: Date.now() }); // ๐Ÿ’ฅ Keeps running after unmount!
    }, 1000);
  }
}

// โœ… Proper cleanup!
class GoodTimer extends Component<{}, { time: number }> {
  private timerRef: number | null = null;
  
  state = { time: 0 };

  componentDidMount(): void {
    this.timerRef = window.setInterval(() => {
      this.setState({ time: Date.now() });
    }, 1000);
  }

  componentWillUnmount(): void {
    if (this.timerRef) {
      clearInterval(this.timerRef); // ๐Ÿงน Clean up!
      this.timerRef = null;
    }
  }

  render(): JSX.Element {
    return <div>โฐ Time: {new Date(this.state.time).toLocaleTimeString()}</div>;
  }
}

๐Ÿ› ๏ธ Best Practices

  1. ๐ŸŽฏ Define Clear Interfaces: Always type your props and state interfaces
  2. ๐Ÿ“ Use Arrow Functions: Avoid binding issues with arrow function methods
  3. ๐Ÿ›ก๏ธ Clean Up Resources: Always cleanup timers, subscriptions, and event listeners
  4. ๐ŸŽจ Extract Complex Logic: Move complex state logic to separate methods
  5. โœจ Use Default Props: Provide sensible defaults with proper typing
  6. ๐Ÿ”„ Avoid Direct State Mutation: Always use setState for immutable updates
  7. ๐Ÿ’ก Keep Render Pure: Render method should be side-effect free

๐Ÿงช Hands-On Exercise

๐ŸŽฏ Challenge: Build a Type-Safe Todo App Class Component

Create a complete todo application using class components:

๐Ÿ“‹ Requirements:

  • โœ… Add, edit, and delete todos
  • ๐Ÿท๏ธ Categories (work, personal, shopping)
  • ๐Ÿ“… Due dates with overdue indicators
  • ๐ŸŽฏ Priority levels (low, medium, high)
  • ๐Ÿ“Š Progress statistics
  • ๐Ÿ’พ Local storage persistence
  • ๐ŸŽจ Each todo needs an emoji based on category!

๐Ÿš€ Bonus Points:

  • Add drag-and-drop reordering
  • Implement undo/redo functionality
  • Create export to JSON feature
  • Add keyboard shortcuts

๐Ÿ’ก Solution

๐Ÿ” Click to see solution
// ๐ŸŽฏ Our type-safe todo app interfaces!
interface Todo {
  id: string;
  title: string;
  description?: string;
  completed: boolean;
  category: 'work' | 'personal' | 'shopping';
  priority: 'low' | 'medium' | 'high';
  dueDate?: Date;
  createdAt: Date;
  updatedAt: Date;
}

interface TodoAppProps {
  storageKey?: string;
}

interface TodoAppState {
  todos: Todo[];
  filter: 'all' | 'active' | 'completed';
  categoryFilter: Todo['category'] | 'all';
  searchTerm: string;
  editingId: string | null;
  showCompleted: boolean;
}

class TodoApp extends Component<TodoAppProps, TodoAppState> {
  static defaultProps: TodoAppProps = {
    storageKey: 'typescript-todos'
  };

  state: TodoAppState = {
    todos: [],
    filter: 'all',
    categoryFilter: 'all',
    searchTerm: '',
    editingId: null,
    showCompleted: true
  };

  // ๐ŸŽฌ Load todos from localStorage
  componentDidMount(): void {
    this.loadTodos();
  }

  // ๐Ÿ’พ Save todos whenever they change
  componentDidUpdate(prevProps: TodoAppProps, prevState: TodoAppState): void {
    if (prevState.todos !== this.state.todos) {
      this.saveTodos();
    }
  }

  // ๐Ÿ“‚ Load from storage
  loadTodos = (): void => {
    try {
      const stored = localStorage.getItem(this.props.storageKey!);
      if (stored) {
        const todos = JSON.parse(stored).map((todo: any) => ({
          ...todo,
          createdAt: new Date(todo.createdAt),
          updatedAt: new Date(todo.updatedAt),
          dueDate: todo.dueDate ? new Date(todo.dueDate) : undefined
        }));
        this.setState({ todos });
      }
    } catch (error) {
      console.error('Failed to load todos:', error);
    }
  };

  // ๐Ÿ’พ Save to storage
  saveTodos = (): void => {
    try {
      localStorage.setItem(
        this.props.storageKey!, 
        JSON.stringify(this.state.todos)
      );
    } catch (error) {
      console.error('Failed to save todos:', error);
    }
  };

  // โœจ Generate emoji based on category
  getCategoryEmoji = (category: Todo['category']): string => {
    const emojis = {
      work: '๐Ÿ’ผ',
      personal: '๐Ÿ ',
      shopping: '๐Ÿ›’'
    };
    return emojis[category];
  };

  // ๐ŸŽฏ Get priority color
  getPriorityColor = (priority: Todo['priority']): string => {
    const colors = {
      low: '#22c55e',
      medium: '#f59e0b',
      high: '#ef4444'
    };
    return colors[priority];
  };

  // โž• Add new todo
  addTodo = (todoData: Omit<Todo, 'id' | 'createdAt' | 'updatedAt'>): void => {
    const newTodo: Todo = {
      ...todoData,
      id: Date.now().toString(),
      createdAt: new Date(),
      updatedAt: new Date()
    };

    this.setState(prevState => ({
      todos: [...prevState.todos, newTodo]
    }));
  };

  // โœ๏ธ Update todo
  updateTodo = (id: string, updates: Partial<Todo>): void => {
    this.setState(prevState => ({
      todos: prevState.todos.map(todo =>
        todo.id === id 
          ? { ...todo, ...updates, updatedAt: new Date() }
          : todo
      ),
      editingId: null
    }));
  };

  // ๐Ÿ—‘๏ธ Delete todo
  deleteTodo = (id: string): void => {
    this.setState(prevState => ({
      todos: prevState.todos.filter(todo => todo.id !== id)
    }));
  };

  // โœ… Toggle completion
  toggleComplete = (id: string): void => {
    this.setState(prevState => ({
      todos: prevState.todos.map(todo =>
        todo.id === id 
          ? { ...todo, completed: !todo.completed, updatedAt: new Date() }
          : todo
      )
    }));
  };

  // ๐Ÿ” Filter todos
  getFilteredTodos = (): Todo[] => {
    const { todos, filter, categoryFilter, searchTerm } = this.state;
    
    return todos.filter(todo => {
      // Filter by completion status
      if (filter === 'active' && todo.completed) return false;
      if (filter === 'completed' && !todo.completed) return false;
      
      // Filter by category
      if (categoryFilter !== 'all' && todo.category !== categoryFilter) return false;
      
      // Filter by search term
      if (searchTerm && !todo.title.toLowerCase().includes(searchTerm.toLowerCase())) {
        return false;
      }
      
      return true;
    });
  };

  // ๐Ÿ“Š Get statistics
  getStats = () => {
    const { todos } = this.state;
    const completed = todos.filter(t => t.completed).length;
    const overdue = todos.filter(t => 
      t.dueDate && t.dueDate < new Date() && !t.completed
    ).length;
    
    return {
      total: todos.length,
      completed,
      active: todos.length - completed,
      overdue,
      completionRate: todos.length > 0 ? Math.round((completed / todos.length) * 100) : 0
    };
  };

  // ๐ŸŽจ Render todo item
  renderTodo = (todo: Todo): JSX.Element => {
    const { editingId } = this.state;
    const isOverdue = todo.dueDate && todo.dueDate < new Date() && !todo.completed;
    
    return (
      <div 
        key={todo.id}
        className={`todo-item ${todo.completed ? 'completed' : ''} ${isOverdue ? 'overdue' : ''}`}
      >
        <div className="todo-main">
          <input
            type="checkbox"
            checked={todo.completed}
            onChange={() => this.toggleComplete(todo.id)}
          />
          
          <div className="todo-content">
            <div className="todo-header">
              <span className="category-emoji">
                {this.getCategoryEmoji(todo.category)}
              </span>
              <h3>{todo.title}</h3>
              <span 
                className="priority-badge"
                style={{ backgroundColor: this.getPriorityColor(todo.priority) }}
              >
                {todo.priority}
              </span>
            </div>
            
            {todo.description && (
              <p className="todo-description">{todo.description}</p>
            )}
            
            {todo.dueDate && (
              <div className="todo-due-date">
                ๐Ÿ—“๏ธ Due: {todo.dueDate.toLocaleDateString()}
                {isOverdue && <span className="overdue-badge">โš ๏ธ Overdue</span>}
              </div>
            )}
          </div>
          
          <div className="todo-actions">
            <button onClick={() => this.setState({ editingId: todo.id })}>
              โœ๏ธ Edit
            </button>
            <button onClick={() => this.deleteTodo(todo.id)}>
              ๐Ÿ—‘๏ธ Delete
            </button>
          </div>
        </div>
      </div>
    );
  };

  // ๐Ÿ—๏ธ Main render
  render(): JSX.Element {
    const { filter, categoryFilter, searchTerm } = this.state;
    const filteredTodos = this.getFilteredTodos();
    const stats = this.getStats();

    return (
      <div className="todo-app">
        <header className="app-header">
          <h1>๐Ÿ“ TypeScript Todo App</h1>
          
          {/* ๐Ÿ“Š Statistics */}
          <div className="stats">
            <div className="stat">
              <span className="stat-value">{stats.total}</span>
              <span className="stat-label">Total</span>
            </div>
            <div className="stat">
              <span className="stat-value">{stats.active}</span>
              <span className="stat-label">Active</span>
            </div>
            <div className="stat">
              <span className="stat-value">{stats.completed}</span>
              <span className="stat-label">Completed</span>
            </div>
            <div className="stat">
              <span className="stat-value">{stats.completionRate}%</span>
              <span className="stat-label">Done</span>
            </div>
            {stats.overdue > 0 && (
              <div className="stat overdue">
                <span className="stat-value">{stats.overdue}</span>
                <span className="stat-label">Overdue</span>
              </div>
            )}
          </div>
        </header>

        {/* ๐Ÿ” Filters */}
        <div className="filters">
          <div className="filter-group">
            <button 
              className={filter === 'all' ? 'active' : ''}
              onClick={() => this.setState({ filter: 'all' })}
            >
              ๐Ÿ“‹ All
            </button>
            <button 
              className={filter === 'active' ? 'active' : ''}
              onClick={() => this.setState({ filter: 'active' })}
            >
              โณ Active
            </button>
            <button 
              className={filter === 'completed' ? 'active' : ''}
              onClick={() => this.setState({ filter: 'completed' })}
            >
              โœ… Completed
            </button>
          </div>

          <select 
            value={categoryFilter}
            onChange={(e) => this.setState({ 
              categoryFilter: e.target.value as Todo['category'] | 'all' 
            })}
          >
            <option value="all">๐Ÿท๏ธ All Categories</option>
            <option value="work">๐Ÿ’ผ Work</option>
            <option value="personal">๐Ÿ  Personal</option>
            <option value="shopping">๐Ÿ›’ Shopping</option>
          </select>

          <input
            type="text"
            placeholder="๐Ÿ” Search todos..."
            value={searchTerm}
            onChange={(e) => this.setState({ searchTerm: e.target.value })}
          />
        </div>

        {/* ๐Ÿ“ Todo List */}
        <div className="todos-container">
          {filteredTodos.length === 0 ? (
            <div className="empty-state">
              {this.state.todos.length === 0 ? (
                <p>๐ŸŽ‰ No todos yet! Add your first task above.</p>
              ) : (
                <p>๐Ÿ” No todos match your current filters.</p>
              )}
            </div>
          ) : (
            filteredTodos.map(this.renderTodo)
          )}
        </div>
      </div>
    );
  }
}

// ๐ŸŽฎ Usage example
const App: React.FC = () => {
  return <TodoApp storageKey="my-awesome-todos" />;
};

๐ŸŽ“ Key Takeaways

Youโ€™ve learned so much! Hereโ€™s what you can now do:

  • โœ… Create type-safe class components with confidence ๐Ÿ’ช
  • โœ… Define proper prop and state interfaces that prevent bugs ๐Ÿ›ก๏ธ
  • โœ… Handle events with correct typing like a pro ๐ŸŽฏ
  • โœ… Manage component lifecycle safely ๐Ÿ”„
  • โœ… Avoid common pitfalls that trip up beginners โš ๏ธ
  • โœ… Apply best practices in legacy codebases ๐Ÿ—๏ธ
  • โœ… Debug class component issues efficiently ๐Ÿ›

Remember: Class components might be โ€œlegacy,โ€ but theyโ€™re still powerful tools in your TypeScript arsenal! ๐Ÿค

๐Ÿค Next Steps

Congratulations! ๐ŸŽ‰ Youโ€™ve mastered React Class Components with TypeScript!

Hereโ€™s what to do next:

  1. ๐Ÿ’ป Practice with the todo app exercise above
  2. ๐Ÿ—๏ธ Refactor an existing class component project
  3. ๐Ÿ“š Move on to our next tutorial: React Hooks: Type-Safe Functional Components
  4. ๐ŸŒŸ Share your class component knowledge with your team!
  5. ๐Ÿ”„ Try converting a class component to a functional component for comparison

Remember: Understanding class components makes you a more complete React developer. Even in the age of hooks, this knowledge is invaluable for maintaining legacy applications! ๐Ÿš€

Happy coding! ๐ŸŽ‰๐Ÿš€โœจ