Prerequisites
- Basic understanding of JavaScript ๐
- TypeScript installation โก
- VS Code or preferred IDE ๐ป
What you'll learn
- Understand Redux fundamentals with TypeScript ๐ฏ
- Apply Redux in real projects with type safety ๐๏ธ
- Debug common Redux TypeScript issues ๐
- Write type-safe Redux code โจ
๐ฏ Introduction
Welcome to the exciting world of Redux with TypeScript! ๐ In this comprehensive guide, weโll explore how to build robust, type-safe state management systems using Redux and TypeScript.
Youโll discover how Redux can transform your TypeScript development experience. Whether youโre building complex web applications ๐, managing global state ๐๏ธ, or creating scalable architectures ๐๏ธ, understanding Redux with TypeScript is essential for writing maintainable, bug-free code.
By the end of this tutorial, youโll feel confident building Redux stores, actions, and reducers with full TypeScript support! Letโs dive in! ๐โโ๏ธ
๐ Understanding Redux with TypeScript
๐ค What is Redux?
Redux is like a central library ๐ for your applicationโs state. Think of it as a well-organized filing cabinet where every piece of data has its place, and you can only access or modify it through specific procedures.
In TypeScript terms, Redux provides predictable state management with strict typing ๐ฏ. This means you can:
- โจ Catch state errors at compile-time
- ๐ Get amazing IDE autocomplete for actions and state
- ๐ก๏ธ Prevent accidental state mutations
- ๐ Self-document your state structure
๐ก Why Use Redux with TypeScript?
Hereโs why developers love this combination:
- Type Safety ๐: Know exactly what your state looks like
- Predictable Updates ๐: State changes follow clear patterns
- Time Travel Debugging โฐ: See exactly how state evolved
- Scalable Architecture ๐๏ธ: Organize complex state logically
Real-world example: Imagine building an e-commerce app ๐. With Redux + TypeScript, you can track user authentication, shopping cart items, product catalogs, and order history with complete type safety!
๐ง Basic Syntax and Usage
๐ Setting Up Redux with TypeScript
Letโs start with a complete Redux setup:
// ๐ฆ Install these packages first:
// npm install @reduxjs/toolkit react-redux
// npm install --save-dev @types/react-redux
// ๐ฏ Define our state types
interface CounterState {
value: number;
status: 'idle' | 'loading' | 'succeeded' | 'failed';
}
// ๐จ Initial state
const initialState: CounterState = {
value: 0,
status: 'idle'
};
// โจ Create a slice (modern Redux approach)
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
const counterSlice = createSlice({
name: 'counter',
initialState,
reducers: {
// ๐ Increment action
increment: (state) => {
state.value += 1;
},
// ๐ Decrement action
decrement: (state) => {
state.value -= 1;
},
// ๐ฏ Set specific value
incrementByAmount: (state, action: PayloadAction<number>) => {
state.value += action.payload;
},
// ๐ Set loading status
setStatus: (state, action: PayloadAction<CounterState['status']>) => {
state.status = action.payload;
}
}
});
// ๐ Export actions and reducer
export const { increment, decrement, incrementByAmount, setStatus } = counterSlice.actions;
export default counterSlice.reducer;
๐ก Explanation: Notice how TypeScript gives us perfect autocomplete and prevents us from passing wrong types to actions!
๐ช Creating the Store
// ๐๏ธ Configure the store
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from './counterSlice';
export const store = configureStore({
reducer: {
counter: counterReducer,
// ๐ฆ Add more reducers here
}
});
// ๐ฏ Infer the `RootState` and `AppDispatch` types
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
๐ก Practical Examples
๐ Example 1: E-Commerce Shopping Cart
Letโs build a realistic shopping cart:
// ๐๏ธ Product interface
interface Product {
id: string;
name: string;
price: number;
emoji: string;
category: string;
}
// ๐ Cart item with quantity
interface CartItem extends Product {
quantity: number;
}
// ๐ช Shopping cart state
interface CartState {
items: CartItem[];
total: number;
itemCount: number;
isOpen: boolean;
}
const initialCartState: CartState = {
items: [],
total: 0,
itemCount: 0,
isOpen: false
};
// ๐จ Create cart slice
const cartSlice = createSlice({
name: 'cart',
initialState: initialCartState,
reducers: {
// โ Add item to cart
addItem: (state, action: PayloadAction<Product>) => {
const existingItem = state.items.find(item => item.id === action.payload.id);
if (existingItem) {
// ๐ Increase quantity
existingItem.quantity += 1;
} else {
// ๐ Add new item
state.items.push({ ...action.payload, quantity: 1 });
}
// ๐ฐ Recalculate totals
cartSlice.caseReducers.calculateTotals(state);
},
// โ Remove item from cart
removeItem: (state, action: PayloadAction<string>) => {
state.items = state.items.filter(item => item.id !== action.payload);
cartSlice.caseReducers.calculateTotals(state);
},
// ๐ข Update quantity
updateQuantity: (state, action: PayloadAction<{id: string, quantity: number}>) => {
const item = state.items.find(item => item.id === action.payload.id);
if (item) {
item.quantity = Math.max(0, action.payload.quantity);
if (item.quantity === 0) {
cartSlice.caseReducers.removeItem(state, { payload: item.id, type: 'removeItem' });
} else {
cartSlice.caseReducers.calculateTotals(state);
}
}
},
// ๐งฎ Calculate totals (helper reducer)
calculateTotals: (state) => {
state.itemCount = state.items.reduce((total, item) => total + item.quantity, 0);
state.total = state.items.reduce((total, item) => total + (item.price * item.quantity), 0);
},
// ๐๏ธ Toggle cart visibility
toggleCart: (state) => {
state.isOpen = !state.isOpen;
},
// ๐งน Clear cart
clearCart: (state) => {
state.items = [];
state.total = 0;
state.itemCount = 0;
}
}
});
export const { addItem, removeItem, updateQuantity, toggleCart, clearCart } = cartSlice.actions;
export default cartSlice.reducer;
// ๐ฎ Usage example
// dispatch(addItem({ id: '1', name: 'TypeScript Book', price: 29.99, emoji: '๐', category: 'books' }));
๐ฎ Example 2: Game State Management
Letโs create a fun game state system:
// ๐ Player stats
interface Player {
id: string;
name: string;
level: number;
experience: number;
health: number;
maxHealth: number;
inventory: Item[];
achievements: Achievement[];
}
// ๐ Inventory item
interface Item {
id: string;
name: string;
type: 'weapon' | 'armor' | 'potion' | 'misc';
emoji: string;
value: number;
quantity: number;
}
// ๐
Achievement system
interface Achievement {
id: string;
title: string;
description: string;
emoji: string;
unlockedAt: Date;
}
// ๐ฎ Game state
interface GameState {
player: Player;
gameStatus: 'menu' | 'playing' | 'paused' | 'game-over';
currentLevel: number;
score: number;
timeElapsed: number;
}
const initialGameState: GameState = {
player: {
id: '1',
name: 'Hero',
level: 1,
experience: 0,
health: 100,
maxHealth: 100,
inventory: [],
achievements: []
},
gameStatus: 'menu',
currentLevel: 1,
score: 0,
timeElapsed: 0
};
// ๐ฏ Game slice
const gameSlice = createSlice({
name: 'game',
initialState: initialGameState,
reducers: {
// ๐ฎ Start new game
startGame: (state) => {
state.gameStatus = 'playing';
state.currentLevel = 1;
state.score = 0;
state.timeElapsed = 0;
},
// โธ๏ธ Pause/resume game
togglePause: (state) => {
state.gameStatus = state.gameStatus === 'playing' ? 'paused' : 'playing';
},
// โญ Gain experience
gainExperience: (state, action: PayloadAction<number>) => {
const player = state.player;
player.experience += action.payload;
// ๐ Level up check
const experienceNeeded = player.level * 100;
if (player.experience >= experienceNeeded) {
player.level += 1;
player.experience -= experienceNeeded;
player.maxHealth += 20;
player.health = player.maxHealth; // Full heal on level up! โจ
// ๐ Achievement for leveling up
const levelAchievement: Achievement = {
id: `level-${player.level}`,
title: `Level ${player.level} Hero`,
description: `Reached level ${player.level}!`,
emoji: 'โญ',
unlockedAt: new Date()
};
player.achievements.push(levelAchievement);
}
},
// ๐ Add item to inventory
addToInventory: (state, action: PayloadAction<Omit<Item, 'quantity'> & {quantity?: number}>) => {
const newItem = { ...action.payload, quantity: action.payload.quantity || 1 };
const existingItem = state.player.inventory.find(item => item.id === newItem.id);
if (existingItem) {
existingItem.quantity += newItem.quantity;
} else {
state.player.inventory.push(newItem);
}
},
// ๐ Use item (like health potion)
useItem: (state, action: PayloadAction<string>) => {
const itemIndex = state.player.inventory.findIndex(item => item.id === action.payload);
if (itemIndex !== -1) {
const item = state.player.inventory[itemIndex];
// ๐ฏ Apply item effects
if (item.type === 'potion' && item.name.includes('Health')) {
state.player.health = Math.min(state.player.maxHealth, state.player.health + 50);
}
// ๐ Decrease quantity
item.quantity -= 1;
if (item.quantity === 0) {
state.player.inventory.splice(itemIndex, 1);
}
}
},
// ๐ Take damage
takeDamage: (state, action: PayloadAction<number>) => {
state.player.health = Math.max(0, state.player.health - action.payload);
if (state.player.health === 0) {
state.gameStatus = 'game-over';
}
},
// ๐ Add points to score
addScore: (state, action: PayloadAction<number>) => {
state.score += action.payload;
}
}
});
export const {
startGame,
togglePause,
gainExperience,
addToInventory,
useItem,
takeDamage,
addScore
} = gameSlice.actions;
export default gameSlice.reducer;
๐ Advanced Concepts
๐งโโ๏ธ Advanced Topic 1: Async Actions with Redux Toolkit
When youโre ready to level up, try async actions:
// ๐ Async thunk for API calls
import { createAsyncThunk } from '@reduxjs/toolkit';
// ๐ค User data interface
interface User {
id: string;
name: string;
email: string;
avatar: string;
}
// ๐ Create async thunk
export const fetchUserById = createAsyncThunk(
'users/fetchById',
async (userId: string, { rejectWithValue }) => {
try {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
throw new Error('Failed to fetch user');
}
const userData: User = await response.json();
return userData;
} catch (error) {
return rejectWithValue(error instanceof Error ? error.message : 'Unknown error');
}
}
);
// ๐ช User slice with async handling
const userSlice = createSlice({
name: 'users',
initialState: {
users: {} as Record<string, User>,
loading: false,
error: null as string | null
},
reducers: {
clearError: (state) => {
state.error = null;
}
},
// ๐ฏ Handle async actions
extraReducers: (builder) => {
builder
.addCase(fetchUserById.pending, (state) => {
state.loading = true;
state.error = null;
})
.addCase(fetchUserById.fulfilled, (state, action) => {
state.loading = false;
state.users[action.payload.id] = action.payload;
})
.addCase(fetchUserById.rejected, (state, action) => {
state.loading = false;
state.error = action.payload as string;
});
}
});
๐๏ธ Advanced Topic 2: Custom Hooks for Redux
For the brave developers, create reusable hooks:
// ๐ช Custom typed hooks
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
import type { RootState, AppDispatch } from './store';
// ๐ฏ Use throughout your app instead of plain `useDispatch` and `useSelector`
export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
// ๐ Custom cart hook
export const useCart = () => {
const dispatch = useAppDispatch();
const cart = useAppSelector(state => state.cart);
const addToCart = (product: Product) => {
dispatch(addItem(product));
};
const removeFromCart = (productId: string) => {
dispatch(removeItem(productId));
};
const updateItemQuantity = (id: string, quantity: number) => {
dispatch(updateQuantity({ id, quantity }));
};
return {
...cart,
addToCart,
removeFromCart,
updateItemQuantity,
toggleCart: () => dispatch(toggleCart()),
clearCart: () => dispatch(clearCart())
};
};
// ๐ฎ Custom game hook
export const useGame = () => {
const dispatch = useAppDispatch();
const game = useAppSelector(state => state.game);
return {
...game,
startNewGame: () => dispatch(startGame()),
pause: () => dispatch(togglePause()),
gainXP: (amount: number) => dispatch(gainExperience(amount)),
heal: () => dispatch(useItem('health-potion')),
attack: (damage: number) => dispatch(takeDamage(damage))
};
};
โ ๏ธ Common Pitfalls and Solutions
๐ฑ Pitfall 1: Mutating State Directly
// โ Wrong way - direct mutation!
const badReducer = (state: CartState, action: PayloadAction<Product>) => {
state.items.push(action.payload); // ๐ฅ This mutates state!
return state;
};
// โ
Correct way - Redux Toolkit uses Immer
const goodReducer = createSlice({
name: 'cart',
initialState,
reducers: {
addItem: (state, action: PayloadAction<Product>) => {
// โจ This looks like mutation but Immer makes it safe!
state.items.push({ ...action.payload, quantity: 1 });
}
}
});
๐คฏ Pitfall 2: Not Typing Actions Properly
// โ Dangerous - no type safety!
const badAction = createAction('INCREMENT');
// โ
Safe - with proper typing!
const goodAction = createAction<number>('INCREMENT');
// ๐ฏ Even better - use createSlice
const counterSlice = createSlice({
name: 'counter',
initialState: { value: 0 },
reducers: {
// ๐ก๏ธ TypeScript infers everything automatically!
increment: (state, action: PayloadAction<number>) => {
state.value += action.payload;
}
}
});
๐ Pitfall 3: Forgetting to Handle Loading States
// โ Missing loading states
interface BadUserState {
user: User | null;
}
// โ
Complete state management
interface GoodUserState {
user: User | null;
loading: boolean;
error: string | null;
lastFetched: Date | null;
}
๐ ๏ธ Best Practices
- ๐ฏ Use Redux Toolkit: Itโs the official, modern way to write Redux
- ๐ Type Everything: Actions, state, and selectors should all be typed
- ๐๏ธ Normalize State: Keep state flat and normalized for complex data
- ๐ Handle All States: Loading, success, error states for async operations
- ๐ช Create Custom Hooks: Encapsulate Redux logic in reusable hooks
- ๐จ Keep Reducers Pure: No side effects, just state transformations
- ๐ฆ Use Slices: Group related actions and reducers together
- ๐ก๏ธ Validate Payloads: Use TypeScript to catch payload errors early
๐งช Hands-On Exercise
๐ฏ Challenge: Build a Todo App with Redux + TypeScript
Create a complete todo application with Redux state management:
๐ Requirements:
- โ Add, edit, delete todos
- ๐ท๏ธ Category filtering (work, personal, shopping)
- ๐ฏ Priority levels (low, medium, high)
- ๐ Progress tracking and statistics
- ๐ Search functionality
- ๐พ Persistent storage simulation
- ๐จ Each todo needs an emoji!
๐ Bonus Points:
- Add due dates with overdue detection
- Implement drag-and-drop reordering
- Add undo/redo functionality
- Create todo templates
๐ก Solution
๐ Click to see solution
// ๐ฏ Todo interfaces
interface Todo {
id: string;
title: string;
description?: string;
completed: boolean;
priority: 'low' | 'medium' | 'high';
category: 'work' | 'personal' | 'shopping';
emoji: string;
dueDate?: Date;
createdAt: Date;
completedAt?: Date;
}
interface TodoState {
todos: Todo[];
filter: {
category: Todo['category'] | 'all';
priority: Todo['priority'] | 'all';
completed: boolean | 'all';
search: string;
};
stats: {
total: number;
completed: number;
overdue: number;
todaysDue: number;
};
}
const initialTodoState: TodoState = {
todos: [],
filter: {
category: 'all',
priority: 'all',
completed: 'all',
search: ''
},
stats: {
total: 0,
completed: 0,
overdue: 0,
todaysDue: 0
}
};
// ๐๏ธ Todo slice
const todoSlice = createSlice({
name: 'todos',
initialState: initialTodoState,
reducers: {
// โ Add new todo
addTodo: (state, action: PayloadAction<Omit<Todo, 'id' | 'createdAt' | 'completed' | 'completedAt'>>) => {
const newTodo: Todo = {
...action.payload,
id: Date.now().toString(),
completed: false,
createdAt: new Date()
};
state.todos.push(newTodo);
todoSlice.caseReducers.updateStats(state);
},
// โ๏ธ Edit todo
editTodo: (state, action: PayloadAction<{id: string, updates: Partial<Omit<Todo, 'id' | 'createdAt'>>}>) => {
const todo = state.todos.find(t => t.id === action.payload.id);
if (todo) {
Object.assign(todo, action.payload.updates);
todoSlice.caseReducers.updateStats(state);
}
},
// โ
Toggle completion
toggleTodo: (state, action: PayloadAction<string>) => {
const todo = state.todos.find(t => t.id === action.payload);
if (todo) {
todo.completed = !todo.completed;
todo.completedAt = todo.completed ? new Date() : undefined;
todoSlice.caseReducers.updateStats(state);
}
},
// ๐๏ธ Delete todo
deleteTodo: (state, action: PayloadAction<string>) => {
state.todos = state.todos.filter(t => t.id !== action.payload);
todoSlice.caseReducers.updateStats(state);
},
// ๐ท๏ธ Set category filter
setCategoryFilter: (state, action: PayloadAction<TodoState['filter']['category']>) => {
state.filter.category = action.payload;
},
// ๐ฏ Set priority filter
setPriorityFilter: (state, action: PayloadAction<TodoState['filter']['priority']>) => {
state.filter.priority = action.payload;
},
// โ
Set completion filter
setCompletedFilter: (state, action: PayloadAction<TodoState['filter']['completed']>) => {
state.filter.completed = action.payload;
},
// ๐ Set search term
setSearchFilter: (state, action: PayloadAction<string>) => {
state.filter.search = action.payload;
},
// ๐งน Clear all filters
clearFilters: (state) => {
state.filter = {
category: 'all',
priority: 'all',
completed: 'all',
search: ''
};
},
// ๐ Update statistics
updateStats: (state) => {
const now = new Date();
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
state.stats.total = state.todos.length;
state.stats.completed = state.todos.filter(t => t.completed).length;
state.stats.overdue = state.todos.filter(t =>
!t.completed && t.dueDate && new Date(t.dueDate) < now
).length;
state.stats.todaysDue = state.todos.filter(t =>
!t.completed && t.dueDate &&
new Date(t.dueDate).toDateString() === today.toDateString()
).length;
}
}
});
export const {
addTodo,
editTodo,
toggleTodo,
deleteTodo,
setCategoryFilter,
setPriorityFilter,
setCompletedFilter,
setSearchFilter,
clearFilters
} = todoSlice.actions;
export default todoSlice.reducer;
// ๐ช Custom hook for todos
export const useTodos = () => {
const dispatch = useAppDispatch();
const todoState = useAppSelector(state => state.todos);
// ๐ Filtered todos
const filteredTodos = todoState.todos.filter(todo => {
const matchesCategory = todoState.filter.category === 'all' || todo.category === todoState.filter.category;
const matchesPriority = todoState.filter.priority === 'all' || todo.priority === todoState.filter.priority;
const matchesCompleted = todoState.filter.completed === 'all' || todo.completed === todoState.filter.completed;
const matchesSearch = todoState.filter.search === '' ||
todo.title.toLowerCase().includes(todoState.filter.search.toLowerCase()) ||
(todo.description && todo.description.toLowerCase().includes(todoState.filter.search.toLowerCase()));
return matchesCategory && matchesPriority && matchesCompleted && matchesSearch;
});
return {
todos: filteredTodos,
allTodos: todoState.todos,
filter: todoState.filter,
stats: todoState.stats,
// ๐ฏ Actions
addTodo: (todo: Omit<Todo, 'id' | 'createdAt' | 'completed' | 'completedAt'>) => dispatch(addTodo(todo)),
editTodo: (id: string, updates: Partial<Omit<Todo, 'id' | 'createdAt'>>) => dispatch(editTodo({ id, updates })),
toggleTodo: (id: string) => dispatch(toggleTodo(id)),
deleteTodo: (id: string) => dispatch(deleteTodo(id)),
setCategoryFilter: (category: TodoState['filter']['category']) => dispatch(setCategoryFilter(category)),
setPriorityFilter: (priority: TodoState['filter']['priority']) => dispatch(setPriorityFilter(priority)),
setCompletedFilter: (completed: TodoState['filter']['completed']) => dispatch(setCompletedFilter(completed)),
setSearchFilter: (search: string) => dispatch(setSearchFilter(search)),
clearFilters: () => dispatch(clearFilters())
};
};
// ๐ฎ Example usage in a React component
const TodoApp: React.FC = () => {
const { todos, stats, addTodo, toggleTodo, setSearchFilter } = useTodos();
const handleAddTodo = () => {
addTodo({
title: "Learn Redux with TypeScript",
priority: "high",
category: "personal",
emoji: "๐",
dueDate: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000) // 7 days from now
});
};
return (
<div>
<h1>๐ฏ My Todos ({stats.completed}/{stats.total})</h1>
{/* Your beautiful UI here! */}
</div>
);
};
๐ Key Takeaways
Youโve learned so much! Hereโs what you can now do:
- โ Create Redux stores with complete TypeScript support ๐ช
- โ Write type-safe actions and reducers that prevent bugs ๐ก๏ธ
- โ Handle async operations with proper loading states ๐
- โ Build custom hooks for reusable Redux logic ๐ช
- โ Apply best practices for scalable state management ๐ฏ
- โ Debug Redux applications with confidence ๐
Remember: Redux with TypeScript is your superpower for building complex, maintainable applications! ๐ฆธโโ๏ธ
๐ค Next Steps
Congratulations! ๐ Youโve mastered Redux with TypeScript!
Hereโs what to do next:
- ๐ป Build the todo app exercise above
- ๐๏ธ Create a more complex app with multiple slices
- ๐ Explore Redux DevTools for debugging
- ๐ Learn about Redux Persist for data persistence
- ๐ Move on to our next tutorial: MobX with TypeScript
Remember: Every Redux expert was once a beginner. Keep building, keep learning, and most importantly, have fun with state management! ๐
Happy coding! ๐๐โจ