Prerequisites
- Basic understanding of JavaScript ๐
- TypeScript installation โก
- VS Code or preferred IDE ๐ป
What you'll learn
- Understand Redux Toolkit fundamentals ๐ฏ
- Apply Redux Toolkit in real projects ๐๏ธ
- Debug common Redux issues ๐
- Write type-safe Redux code โจ
๐ฏ Introduction
Welcome to the world of modern Redux with TypeScript! ๐ Redux Toolkit (RTK) is the official way to write Redux logic - itโs like upgrading from a bicycle to a rocket ship! ๐
Redux used to be complex and verbose, but Redux Toolkit makes state management a joy. Youโll discover how RTK eliminates boilerplate, provides excellent TypeScript support, and includes powerful tools like RTK Query for data fetching.
By the end of this tutorial, youโll be building type-safe, scalable Redux applications with confidence! Letโs revolutionize your state management! ๐ช
๐ Understanding Redux Toolkit
๐ค What is Redux Toolkit?
Redux Toolkit is like having a Swiss Army knife ๐ง for state management. Think of regular Redux as building a house with individual nails and boards, while RTK is like having pre-fabricated walls and power tools!
In TypeScript terms, Redux Toolkit provides:
- โจ Less boilerplate: Write 75% less code
- ๐ Better defaults: Sensible configurations out of the box
- ๐ก๏ธ Built-in safety: Immutable updates with Immer
- ๐ฆ Integrated tools: DevTools, thunk middleware included
๐ก Why Use Redux Toolkit?
Hereโs why developers love RTK:
- Type Safety ๐: Full TypeScript support with proper inference
- Developer Experience ๐ป: Amazing DevTools and debugging
- Performance โก: Optimized selectors and updates
- Maintainability ๐ง: Less code means fewer bugs
Real-world example: Imagine managing a shopping app state ๐. With RTK, you can handle user auth, cart items, and API calls with clean, type-safe code!
๐ง Basic Syntax and Usage
๐ Setting Up Redux Toolkit
Letโs start with installation and basic setup:
# ๐ฆ Install Redux Toolkit and React-Redux
npm install @reduxjs/toolkit react-redux
npm install -D @types/react-redux
// ๐ช store/store.ts - Creating our store
import { configureStore } from '@reduxjs/toolkit';
import counterSlice from './counterSlice';
import userSlice from './userSlice';
// ๐ฏ Configure store with slices
export const store = configureStore({
reducer: {
counter: counterSlice,
user: userSlice,
},
});
// ๐จ Infer types from store
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
๐ก Explanation: configureStore
automatically sets up DevTools, middleware, and more. The type inference gives us perfect TypeScript support!
๐ฏ Creating Your First Slice
Hereโs the magic of slices - they combine actions and reducers:
// ๐ฐ store/counterSlice.ts - A slice contains everything!
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
// ๐จ Define our state shape
interface CounterState {
value: number;
status: 'idle' | 'loading' | 'succeeded' | 'failed';
emoji: string;
}
// ๐ Initial state
const initialState: CounterState = {
value: 0,
status: 'idle',
emoji: '๐ฏ'
};
// ๐ฐ Create slice with actions and reducers
const counterSlice = createSlice({
name: 'counter',
initialState,
reducers: {
// โ Increment action
increment: (state) => {
state.value += 1; // ๐ช Immer makes this safe!
state.emoji = '๐';
},
// โ Decrement action
decrement: (state) => {
state.value -= 1;
state.emoji = '๐';
},
// ๐ฏ Add specific amount
incrementByAmount: (state, action: PayloadAction<number>) => {
state.value += action.payload;
state.emoji = action.payload > 10 ? '๐' : 'โจ';
},
// ๐ฎ Reset counter
reset: (state) => {
state.value = 0;
state.status = 'idle';
state.emoji = '๐ฏ';
}
}
});
// ๐ช Export actions and reducer
export const { increment, decrement, incrementByAmount, reset } = counterSlice.actions;
export default counterSlice.reducer;
๐ก Practical Examples
๐ Example 1: Shopping Cart Management
Letโs build a real shopping cart with RTK:
// ๐๏ธ store/cartSlice.ts - Shopping cart slice
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
interface Product {
id: string;
name: string;
price: number;
emoji: string;
image: string;
}
interface CartItem extends Product {
quantity: number;
}
interface CartState {
items: CartItem[];
total: number;
shipping: number;
status: 'idle' | 'loading' | 'succeeded' | 'failed';
}
const initialState: CartState = {
items: [],
total: 0,
shipping: 0,
status: 'idle'
};
const cartSlice = createSlice({
name: 'cart',
initialState,
reducers: {
// ๐ Add item to cart
addItem: (state, action: PayloadAction<Product>) => {
const existingItem = state.items.find(item => item.id === action.payload.id);
if (existingItem) {
existingItem.quantity += 1; // ๐ Increase quantity
} else {
state.items.push({ ...action.payload, quantity: 1 }); // โ Add new item
}
// ๐ฐ Recalculate total
cartSlice.caseReducers.calculateTotal(state);
},
// ๐๏ธ Remove item
removeItem: (state, action: PayloadAction<string>) => {
state.items = state.items.filter(item => item.id !== action.payload);
cartSlice.caseReducers.calculateTotal(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) {
state.items = state.items.filter(i => i.id !== action.payload.id);
}
}
cartSlice.caseReducers.calculateTotal(state);
},
// ๐งน Clear cart
clearCart: (state) => {
state.items = [];
state.total = 0;
},
// ๐ฐ Calculate total (internal reducer)
calculateTotal: (state) => {
const subtotal = state.items.reduce((sum, item) => sum + (item.price * item.quantity), 0);
state.shipping = subtotal > 50 ? 0 : 5.99; // ๐ Free shipping over $50
state.total = subtotal + state.shipping;
}
}
});
export const { addItem, removeItem, updateQuantity, clearCart } = cartSlice.actions;
export default cartSlice.reducer;
๐ฏ Try it yourself: Add a discount code feature and wishlist functionality!
๐ค Example 2: User Authentication Slice
Letโs handle user auth with async actions:
// ๐ store/authSlice.ts - Authentication with async thunks
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
interface User {
id: string;
email: string;
name: string;
avatar: string;
role: 'user' | 'admin';
}
interface AuthState {
user: User | null;
token: string | null;
isLoading: boolean;
error: string | null;
isAuthenticated: boolean;
}
const initialState: AuthState = {
user: null,
token: localStorage.getItem('token'),
isLoading: false,
error: null,
isAuthenticated: false
};
// ๐ Async thunk for login
export const loginUser = createAsyncThunk(
'auth/login',
async (credentials: {email: string, password: string}, { rejectWithValue }) => {
try {
// ๐ API call simulation
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(credentials)
});
if (!response.ok) {
throw new Error('Login failed! ๐');
}
const data = await response.json();
localStorage.setItem('token', data.token); // ๐พ Store token
return data;
} catch (error) {
return rejectWithValue(error instanceof Error ? error.message : 'Unknown error');
}
}
);
// ๐ช Async thunk for logout
export const logoutUser = createAsyncThunk(
'auth/logout',
async (_, { dispatch }) => {
localStorage.removeItem('token'); // ๐๏ธ Remove token
return null;
}
);
const authSlice = createSlice({
name: 'auth',
initialState,
reducers: {
// ๐งน Clear error
clearError: (state) => {
state.error = null;
},
// ๐ Reset auth state
resetAuth: (state) => {
state.user = null;
state.token = null;
state.isAuthenticated = false;
state.error = null;
}
},
extraReducers: (builder) => {
builder
// ๐ Login cases
.addCase(loginUser.pending, (state) => {
state.isLoading = true;
state.error = null;
})
.addCase(loginUser.fulfilled, (state, action) => {
state.isLoading = false;
state.user = action.payload.user;
state.token = action.payload.token;
state.isAuthenticated = true;
state.error = null;
})
.addCase(loginUser.rejected, (state, action) => {
state.isLoading = false;
state.error = action.payload as string;
state.isAuthenticated = false;
})
// ๐ช Logout cases
.addCase(logoutUser.fulfilled, (state) => {
state.user = null;
state.token = null;
state.isAuthenticated = false;
});
}
});
export const { clearError, resetAuth } = authSlice.actions;
export default authSlice.reducer;
๐ Advanced Concepts
๐งโโ๏ธ Advanced Topic 1: RTK Query for Data Fetching
When youโre ready to level up, RTK Query is magical:
// ๐ store/api.ts - RTK Query API slice
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
interface Post {
id: number;
title: string;
body: string;
userId: number;
reactions: {
likes: number;
dislikes: number;
};
}
interface User {
id: number;
name: string;
email: string;
avatar: string;
}
// ๐ฏ Create API slice
export const apiSlice = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({
baseUrl: '/api/',
prepareHeaders: (headers, { getState }) => {
// ๐ Add auth token to requests
const token = (getState() as any).auth.token;
if (token) {
headers.set('authorization', `Bearer ${token}`);
}
return headers;
},
}),
tagTypes: ['Post', 'User'],
endpoints: (builder) => ({
// ๐ Get posts
getPosts: builder.query<Post[], void>({
query: () => 'posts',
providesTags: ['Post'],
}),
// ๐ Get single post
getPost: builder.query<Post, number>({
query: (id) => `posts/${id}`,
providesTags: (result, error, id) => [{ type: 'Post', id }],
}),
// โ Create post
createPost: builder.mutation<Post, Partial<Post>>({
query: (post) => ({
url: 'posts',
method: 'POST',
body: post,
}),
invalidatesTags: ['Post'],
}),
// ๐ค Get user profile
getUser: builder.query<User, number>({
query: (id) => `users/${id}`,
providesTags: (result, error, id) => [{ type: 'User', id }],
}),
}),
});
// ๐ช Export hooks for use in components
export const {
useGetPostsQuery,
useGetPostQuery,
useCreatePostMutation,
useGetUserQuery,
} = apiSlice;
๐๏ธ Advanced Topic 2: Custom Middleware and Enhancers
For the brave developers:
// ๐ ๏ธ store/middleware.ts - Custom middleware
import { Middleware } from '@reduxjs/toolkit';
// ๐ Logger middleware with emojis
export const emojiLogger: Middleware = (store) => (next) => (action) => {
console.log('๐ฌ Action:', action.type);
console.log('๐ Current State:', store.getState());
const result = next(action);
console.log('๐ New State:', store.getState());
console.log('---');
return result;
};
// ๐ Notification middleware
export const notificationMiddleware: Middleware = (store) => (next) => (action) => {
const result = next(action);
// ๐ฏ Show notifications for specific actions
if (action.type.endsWith('/fulfilled')) {
const actionName = action.type.replace('/fulfilled', '');
console.log(`โ
${actionName} completed successfully!`);
}
if (action.type.endsWith('/rejected')) {
const actionName = action.type.replace('/rejected', '');
console.log(`โ ${actionName} failed!`);
}
return result;
};
โ ๏ธ Common Pitfalls and Solutions
๐ฑ Pitfall 1: Mutating State Directly
// โ Wrong way - direct mutation without Immer!
const badSlice = createSlice({
name: 'bad',
initialState: { items: [] },
reducers: {
addItem: (state, action) => {
// ๐ฅ This breaks outside of RTK!
state.items.push(action.payload);
return state; // โ Don't return state in RTK
}
}
});
// โ
Correct way - let Immer handle it!
const goodSlice = createSlice({
name: 'good',
initialState: { items: [] as string[] },
reducers: {
addItem: (state, action: PayloadAction<string>) => {
// โจ Immer makes this safe - no return needed!
state.items.push(action.payload);
}
}
});
๐คฏ Pitfall 2: Forgetting to Add Slice to Store
// โ Created slice but forgot to add to store!
export const store = configureStore({
reducer: {
counter: counterSlice,
// ๐ฑ Forgot to add cartSlice!
},
});
// โ
Remember to add all slices!
export const store = configureStore({
reducer: {
counter: counterSlice,
cart: cartSlice, // โ
Added!
auth: authSlice, // โ
Added!
api: apiSlice.reducer, // โ
Don't forget RTK Query!
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(apiSlice.middleware),
});
๐ ๏ธ Best Practices
- ๐ฏ One Concern Per Slice: Keep slices focused on single features
- ๐ Use TypeScript: Always type your state and actions
- ๐ Normalize Data: Use normalized state structure for complex data
- โก Use RTK Query: Let it handle API calls and caching
- ๐งช Test Your Slices: Write unit tests for reducers
๐งช Hands-On Exercise
๐ฏ Challenge: Build a Recipe Manager
Create a type-safe recipe management system:
๐ Requirements:
- โ Recipe CRUD operations (Create, Read, Update, Delete)
- ๐ท๏ธ Categories and tags for recipes
- โญ Rating and favorites system
- ๐ Search and filter functionality
- ๐ฑ API integration with RTK Query
- ๐จ Each recipe needs cooking emojis!
๐ Bonus Points:
- Add shopping list generation from recipes
- Implement recipe sharing
- Create meal planning features
๐ก Solution
๐ Click to see solution
// ๐ณ store/recipesSlice.ts - Recipe management slice
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
interface Recipe {
id: string;
title: string;
description: string;
ingredients: string[];
instructions: string[];
cookingTime: number;
difficulty: 'easy' | 'medium' | 'hard';
category: 'breakfast' | 'lunch' | 'dinner' | 'dessert' | 'snack';
tags: string[];
rating: number;
emoji: string;
image?: string;
isFavorite: boolean;
}
interface RecipesState {
recipes: Recipe[];
favorites: string[];
searchTerm: string;
selectedCategory: string | null;
sortBy: 'name' | 'rating' | 'cookingTime' | 'difficulty';
isLoading: boolean;
}
const initialState: RecipesState = {
recipes: [],
favorites: [],
searchTerm: '',
selectedCategory: null,
sortBy: 'name',
isLoading: false
};
const recipesSlice = createSlice({
name: 'recipes',
initialState,
reducers: {
// โ Add new recipe
addRecipe: (state, action: PayloadAction<Omit<Recipe, 'id'>>) => {
const newRecipe: Recipe = {
...action.payload,
id: Date.now().toString(),
isFavorite: false
};
state.recipes.push(newRecipe);
},
// โ๏ธ Update recipe
updateRecipe: (state, action: PayloadAction<Recipe>) => {
const index = state.recipes.findIndex(r => r.id === action.payload.id);
if (index !== -1) {
state.recipes[index] = action.payload;
}
},
// ๐๏ธ Delete recipe
deleteRecipe: (state, action: PayloadAction<string>) => {
state.recipes = state.recipes.filter(r => r.id !== action.payload);
state.favorites = state.favorites.filter(id => id !== action.payload);
},
// โญ Toggle favorite
toggleFavorite: (state, action: PayloadAction<string>) => {
const recipe = state.recipes.find(r => r.id === action.payload);
if (recipe) {
recipe.isFavorite = !recipe.isFavorite;
if (recipe.isFavorite) {
state.favorites.push(recipe.id);
} else {
state.favorites = state.favorites.filter(id => id !== recipe.id);
}
}
},
// ๐ Set search term
setSearchTerm: (state, action: PayloadAction<string>) => {
state.searchTerm = action.payload;
},
// ๐ท๏ธ Set category filter
setSelectedCategory: (state, action: PayloadAction<string | null>) => {
state.selectedCategory = action.payload;
},
// ๐ Set sort order
setSortBy: (state, action: PayloadAction<RecipesState['sortBy']>) => {
state.sortBy = action.payload;
}
}
});
export const {
addRecipe,
updateRecipe,
deleteRecipe,
toggleFavorite,
setSearchTerm,
setSelectedCategory,
setSortBy
} = recipesSlice.actions;
export default recipesSlice.reducer;
// ๐ฏ Selectors
export const selectAllRecipes = (state: { recipes: RecipesState }) => state.recipes.recipes;
export const selectFavoriteRecipes = (state: { recipes: RecipesState }) =>
state.recipes.recipes.filter(recipe => recipe.isFavorite);
export const selectRecipesByCategory = (state: { recipes: RecipesState }, category: string) =>
state.recipes.recipes.filter(recipe => recipe.category === category);
๐ Key Takeaways
Youโve mastered Redux Toolkit! Hereโs what you can now do:
- โ Create type-safe slices with actions and reducers ๐ช
- โ Handle async operations with createAsyncThunk ๐ก๏ธ
- โ Use RTK Query for efficient data fetching ๐ฏ
- โ Debug Redux apps with DevTools ๐
- โ Build scalable applications with modern Redux! ๐
Remember: Redux Toolkit makes state management enjoyable, not overwhelming! Itโs your friend for building amazing apps. ๐ค
๐ค Next Steps
Congratulations! ๐ Youโve mastered Redux Toolkit with TypeScript!
Hereโs what to do next:
- ๐ป Practice with the recipe manager exercise
- ๐๏ธ Build a project using RTK and RTK Query
- ๐ Explore React-Redux hooks (useSelector, useDispatch)
- ๐ Learn about Redux DevTools time-travel debugging
Remember: Every Redux expert started as a beginner. Keep building, keep learning, and most importantly, enjoy creating awesome applications! ๐
Your state management journey has just leveled up! ๐ฎโจ
Happy coding! ๐๐โจ