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:
- Type Safety ๐: Catch prop and state errors at compile-time
- Better IDE Support ๐ป: Amazing autocomplete and refactoring
- Legacy Code Maintenance ๐: Easier to understand and modify old code
- 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
- ๐ฏ Define Clear Interfaces: Always type your props and state interfaces
- ๐ Use Arrow Functions: Avoid binding issues with arrow function methods
- ๐ก๏ธ Clean Up Resources: Always cleanup timers, subscriptions, and event listeners
- ๐จ Extract Complex Logic: Move complex state logic to separate methods
- โจ Use Default Props: Provide sensible defaults with proper typing
- ๐ Avoid Direct State Mutation: Always use setState for immutable updates
- ๐ก 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:
- ๐ป Practice with the todo app exercise above
- ๐๏ธ Refactor an existing class component project
- ๐ Move on to our next tutorial: React Hooks: Type-Safe Functional Components
- ๐ Share your class component knowledge with your team!
- ๐ 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! ๐๐โจ