Complete Guide from Noob to Pro
Definition: JavaScript is a programming language that makes websites interactive. Think of it like teaching a robot to do tasks.
// Variables are like boxes where you store things
let toyBox = "Car"; // You can change what's inside
const name = "Rajneesh"; // This cannot be changed
// Objects are like a bag with multiple pockets
const student = {
name: "Rajneesh",
age: 20,
course: "Redux"
};
// Arrays are like a list
const fruits = ["Apple", "Banana", "Mango"];
// Arrow Functions (shortcut to write functions)
const greet = (name) => {
return `Hello ${name}!`;
};
console.log(greet("Rajneesh")); // Output: Hello Rajneesh!
Definition: Spread operator allows you to copy or combine arrays/objects easily.
// Imagine you have two toy boxes
const box1 = ["Car", "Ball"];
const box2 = ["Doll", "Puzzle"];
// Spread operator combines them
const allToys = [...box1, ...box2];
console.log(allToys); // ["Car", "Ball", "Doll", "Puzzle"]
// With objects
const person = { name: "Raj", age: 20 };
const updatedPerson = { ...person, city: "Delhi" };
console.log(updatedPerson); // { name: "Raj", age: 20, city: "Delhi" }
Definition: React is a library to build user interfaces. Think of it like building with LEGO blocks - each block is a component.
// Component is like a reusable template
function Button(props) {
return ;
}
// Using the component
function App() {
return (
);
}
Definition: useState is a hook that lets components remember things. Like a notebook where you write and update notes.
import { useState } from 'react';
function Counter() {
// count is the value, setCount is the function to change it
const [count, setCount] = useState(0);
return (
Count: {count}
);
}
Prop Drilling Problem: Imagine passing a message through 10 people to reach the last person. That's prop drilling - passing data through many components.
// Grandparent has the data
function Grandparent() {
const user = { name: "Rajneesh" };
return ;
}
// Parent doesn't need it, just passes it
function Parent({ user }) {
return ;
}
// Child finally uses it
function Child({ user }) {
return Hello {user.name}
;
}
// This is annoying! Redux solves this.
Redux creates a central store (like a warehouse) where all components can directly get data without passing through others.
Simple Explanation: Imagine a big warehouse (Store) where all your app's data is kept. Instead of each room (component) having its own storage, everyone goes to the warehouse to get or update things.
Definition: The entire state of your application is stored in one object tree within a single store.
Simple: All data lives in one place, not scattered everywhere.
// The entire app state in one object
const state = {
user: { name: "Rajneesh", loggedIn: true },
todos: ["Learn Redux", "Build Project"],
cart: { items: [], total: 0 }
};
Definition: The only way to change state is to emit an action, an object describing what happened.
Simple: You can't directly change data. You must send a request (action) to change it.
// ❌ Wrong - Direct mutation
state.user.name = "New Name";
// ✅ Correct - Dispatch an action
dispatch({
type: "UPDATE_NAME",
payload: "New Name"
});
Definition: Reducers are pure functions that take the previous state and an action, and return the next state.
Pure Function: Same input always gives same output, no side effects.
// Pure function - predictable
function add(a, b) {
return a + b; // Always same result for same inputs
}
// Impure function - unpredictable
function addRandom(a) {
return a + Math.random(); // Different result each time
}
// Redux reducer (pure function)
function counterReducer(state = 0, action) {
if (action.type === "INCREMENT") {
return state + 1;
}
return state;
}
// 1. Define Action Types
const INCREMENT = "INCREMENT";
const DECREMENT = "DECREMENT";
// 2. Create Actions (Request forms)
const increment = () => ({ type: INCREMENT });
const decrement = () => ({ type: DECREMENT });
// 3. Create Reducer (Warehouse worker)
function counterReducer(state = 0, action) {
switch(action.type) {
case INCREMENT:
return state + 1;
case DECREMENT:
return state - 1;
default:
return state;
}
}
// 4. Create Store (Warehouse)
import { createStore } from 'redux';
const store = createStore(counterReducer);
// 5. Subscribe to changes (Get notifications)
store.subscribe(() => {
console.log("State changed:", store.getState());
});
// 6. Dispatch actions (Submit requests)
store.dispatch(increment()); // State: 1
store.dispatch(increment()); // State: 2
store.dispatch(decrement()); // State: 1
Note This is old Redux. We don't use this in real projects, but it helps understand the concept.
// ========== COMPLETE REDUX EXAMPLE ==========
// 1. ACTION TYPES (Constants)
const ADD_TODO = "ADD_TODO";
const TOGGLE_TODO = "TOGGLE_TODO";
const DELETE_TODO = "DELETE_TODO";
// 2. ACTION CREATORS (Functions that return actions)
const addTodo = (text) => ({
type: ADD_TODO,
payload: {
id: Date.now(),
text: text,
completed: false
}
});
const toggleTodo = (id) => ({
type: TOGGLE_TODO,
payload: id
});
const deleteTodo = (id) => ({
type: DELETE_TODO,
payload: id
});
// 3. REDUCER (Pure function that updates state)
const initialState = {
todos: []
};
function todoReducer(state = initialState, action) {
switch(action.type) {
case ADD_TODO:
return {
...state,
todos: [...state.todos, action.payload]
};
case TOGGLE_TODO:
return {
...state,
todos: state.todos.map(todo =>
todo.id === action.payload
? { ...todo, completed: !todo.completed }
: todo
)
};
case DELETE_TODO:
return {
...state,
todos: state.todos.filter(todo => todo.id !== action.payload)
};
default:
return state;
}
}
// 4. CREATE STORE
import { createStore } from 'redux';
const store = createStore(todoReducer);
// 5. SUBSCRIBE (Listen to changes)
store.subscribe(() => {
console.log("Current State:", store.getState());
});
// 6. DISPATCH ACTIONS (Use the store)
store.dispatch(addTodo("Learn Redux"));
store.dispatch(addTodo("Build Project"));
store.dispatch(toggleTodo(1));
store.dispatch(deleteTodo(2));
console.log("Final State:", store.getState());
Definition: Creates the Redux store with good defaults. Automatically sets up Redux DevTools and middleware.
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from './counterSlice';
import userReducer from './userSlice';
// Create store - super simple!
const store = configureStore({
reducer: {
counter: counterReducer,
user: userReducer
}
});
export default store;
// Old Redux way (much more code):
// import { createStore, combineReducers, applyMiddleware } from 'redux';
// import thunk from 'redux-thunk';
// import { composeWithDevTools } from 'redux-devtools-extension';
// const rootReducer = combineReducers({ counter, user });
// const store = createStore(rootReducer, composeWithDevTools(applyMiddleware(thunk)));
Definition: A function that accepts an initial state, an object of reducer functions, and a "slice name", and automatically generates action creators and action types.
Simple: One function to create actions and reducers together!
import { createSlice } from '@reduxjs/toolkit';
// Create a slice (piece of state)
const counterSlice = createSlice({
name: 'counter', // Name of this slice
initialState: {
value: 0
},
reducers: {
// Actions are automatically created!
increment: (state) => {
// Looks like mutation, but Immer makes it immutable
state.value += 1;
},
decrement: (state) => {
state.value -= 1;
},
incrementByAmount: (state, action) => {
state.value += action.payload;
},
reset: (state) => {
state.value = 0;
}
}
});
// Export actions (automatically created)
export const { increment, decrement, incrementByAmount, reset } = counterSlice.actions;
// Export reducer
export default counterSlice.reducer;
// Usage in component:
// import { increment } from './counterSlice';
// dispatch(increment());
Definition: A function that accepts a Redux action type string and a callback function that returns a promise. It generates promise lifecycle action types based on the action type prefix.
Simple: Easy way to handle API calls in Redux!
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import axios from 'axios';
// Create async thunk for API call
export const fetchUsers = createAsyncThunk(
'users/fetchUsers', // Action type prefix
async () => {
const response = await axios.get('https://api.example.com/users');
return response.data; // This becomes action.payload
}
);
const usersSlice = createSlice({
name: 'users',
initialState: {
users: [],
loading: false,
error: null
},
reducers: {},
extraReducers: (builder) => {
builder
// When API call starts
.addCase(fetchUsers.pending, (state) => {
state.loading = true;
state.error = null;
})
// When API call succeeds
.addCase(fetchUsers.fulfilled, (state, action) => {
state.loading = false;
state.users = action.payload;
})
// When API call fails
.addCase(fetchUsers.rejected, (state, action) => {
state.loading = false;
state.error = action.error.message;
});
}
});
export default usersSlice.reducer;
// Usage in component:
// dispatch(fetchUsers());
// ========== store.js ==========
import { configureStore } from '@reduxjs/toolkit';
import todosReducer from '../features/todos/todosSlice';
export const store = configureStore({
reducer: {
todos: todosReducer
}
});
// ========== todosSlice.js ==========
import { createSlice } from '@reduxjs/toolkit';
const todosSlice = createSlice({
name: 'todos',
initialState: {
items: [],
filter: 'all' // all, active, completed
},
reducers: {
addTodo: (state, action) => {
state.items.push({
id: Date.now(),
text: action.payload,
completed: false
});
},
toggleTodo: (state, action) => {
const todo = state.items.find(t => t.id === action.payload);
if (todo) {
todo.completed = !todo.completed;
}
},
deleteTodo: (state, action) => {
state.items = state.items.filter(t => t.id !== action.payload);
},
editTodo: (state, action) => {
const todo = state.items.find(t => t.id === action.payload.id);
if (todo) {
todo.text = action.payload.text;
}
},
setFilter: (state, action) => {
state.filter = action.payload;
},
clearCompleted: (state) => {
state.items = state.items.filter(t => !t.completed);
}
}
});
export const {
addTodo,
toggleTodo,
deleteTodo,
editTodo,
setFilter,
clearCompleted
} = todosSlice.actions;
// Selectors
export const selectAllTodos = (state) => state.todos.items;
export const selectFilter = (state) => state.todos.filter;
export const selectFilteredTodos = (state) => {
const { items, filter } = state.todos;
if (filter === 'active') return items.filter(t => !t.completed);
if (filter === 'completed') return items.filter(t => t.completed);
return items;
};
export default todosSlice.reducer;
// ========== TodoList.jsx ==========
import React, { useState } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import {
addTodo,
toggleTodo,
deleteTodo,
setFilter,
clearCompleted,
selectFilteredTodos,
selectFilter
} from './todosSlice';
function TodoList() {
const [input, setInput] = useState('');
const todos = useSelector(selectFilteredTodos);
const filter = useSelector(selectFilter);
const dispatch = useDispatch();
const handleSubmit = (e) => {
e.preventDefault();
if (input.trim()) {
dispatch(addTodo(input));
setInput('');
}
};
return (
Redux Todo App
{todos.map(todo => (
-
dispatch(toggleTodo(todo.id))}
/>
{todo.text}
))}
);
}
export default TodoList;
// ========== main.jsx ==========
import React from 'react';
import ReactDOM from 'react-dom/client';
import { Provider } from 'react-redux';
import { store } from './app/store';
import App from './App';
ReactDOM.createRoot(document.getElementById('root')).render(
);
// ========== usersSlice.js ==========
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import axios from 'axios';
const API_URL = 'https://jsonplaceholder.typicode.com/users';
// Async thunk to fetch users
export const fetchUsers = createAsyncThunk(
'users/fetchUsers',
async (_, { rejectWithValue }) => {
try {
const response = await axios.get(API_URL);
return response.data;
} catch (error) {
return rejectWithValue(error.response.data);
}
}
);
// Async thunk to add user
export const addUser = createAsyncThunk(
'users/addUser',
async (userData, { rejectWithValue }) => {
try {
const response = await axios.post(API_URL, userData);
return response.data;
} catch (error) {
return rejectWithValue(error.response.data);
}
}
);
// Async thunk to update user
export const updateUser = createAsyncThunk(
'users/updateUser',
async ({ id, userData }, { rejectWithValue }) => {
try {
const response = await axios.put(`${API_URL}/${id}`, userData);
return response.data;
} catch (error) {
return rejectWithValue(error.response.data);
}
}
);
// Async thunk to delete user
export const deleteUser = createAsyncThunk(
'users/deleteUser',
async (id, { rejectWithValue }) => {
try {
await axios.delete(`${API_URL}/${id}`);
return id;
} catch (error) {
return rejectWithValue(error.response.data);
}
}
);
const usersSlice = createSlice({
name: 'users',
initialState: {
users: [],
loading: false,
error: null,
success: false
},
reducers: {
clearError: (state) => {
state.error = null;
},
clearSuccess: (state) => {
state.success = false;
}
},
extraReducers: (builder) => {
builder
// Fetch Users
.addCase(fetchUsers.pending, (state) => {
state.loading = true;
state.error = null;
})
.addCase(fetchUsers.fulfilled, (state, action) => {
state.loading = false;
state.users = action.payload;
})
.addCase(fetchUsers.rejected, (state, action) => {
state.loading = false;
state.error = action.payload || 'Failed to fetch users';
})
// Add User
.addCase(addUser.pending, (state) => {
state.loading = true;
})
.addCase(addUser.fulfilled, (state, action) => {
state.loading = false;
state.users.push(action.payload);
state.success = true;
})
.addCase(addUser.rejected, (state, action) => {
state.loading = false;
state.error = action.payload || 'Failed to add user';
})
// Update User
.addCase(updateUser.fulfilled, (state, action) => {
const index = state.users.findIndex(u => u.id === action.payload.id);
if (index !== -1) {
state.users[index] = action.payload;
}
state.success = true;
})
// Delete User
.addCase(deleteUser.fulfilled, (state, action) => {
state.users = state.users.filter(u => u.id !== action.payload);
state.success = true;
});
}
});
export const { clearError, clearSuccess } = usersSlice.actions;
export default usersSlice.reducer;
// ========== UserList.jsx ==========
import React, { useEffect, useState } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import {
fetchUsers,
addUser,
updateUser,
deleteUser,
clearError,
clearSuccess
} from './usersSlice';
function UserList() {
const dispatch = useDispatch();
const { users, loading, error, success } = useSelector(state => state.users);
const [formData, setFormData] = useState({
name: '',
email: '',
phone: ''
});
const [editingId, setEditingId] = useState(null);
useEffect(() => {
dispatch(fetchUsers());
}, [dispatch]);
useEffect(() => {
if (success) {
alert('Operation successful!');
dispatch(clearSuccess());
setFormData({ name: '', email: '', phone: '' });
setEditingId(null);
}
}, [success, dispatch]);
const handleSubmit = (e) => {
e.preventDefault();
if (editingId) {
dispatch(updateUser({ id: editingId, userData: formData }));
} else {
dispatch(addUser(formData));
}
};
const handleEdit = (user) => {
setFormData({
name: user.name,
email: user.email,
phone: user.phone
});
setEditingId(user.id);
};
const handleDelete = (id) => {
if (window.confirm('Are you sure?')) {
dispatch(deleteUser(id));
}
};
if (loading) {
return Loading...;
}
return (
User Management
{error && (
{error}
)}
{users.map(user => (
{user.name}
📧 {user.email}
📱 {user.phone}
))}
);
}
export default UserList;
// ========== Advanced Error Handling ==========
// 1. Create a custom error handler
export const handleAsyncError = (error) => {
if (error.response) {
// Server responded with error
return {
message: error.response.data.message || 'Server error',
status: error.response.status
};
} else if (error.request) {
// Request made but no response
return {
message: 'No response from server',
status: 0
};
} else {
// Something else happened
return {
message: error.message || 'Unknown error',
status: -1
};
}
};
// 2. Use in async thunk
export const fetchData = createAsyncThunk(
'data/fetch',
async (_, { rejectWithValue }) => {
try {
const response = await axios.get('/api/data');
return response.data;
} catch (error) {
const errorData = handleAsyncError(error);
return rejectWithValue(errorData);
}
}
);
// 3. Global error toast component
function ErrorToast() {
const error = useSelector(state => state.app.error);
const dispatch = useDispatch();
useEffect(() => {
if (error) {
const timer = setTimeout(() => {
dispatch(clearError());
}, 5000);
return () => clearTimeout(timer);
}
}, [error, dispatch]);
if (!error) return null;
return (
{error.message}
);
}
const state = {
posts: [
{
id: 1,
title: "Post 1",
author: { id: 1, name: "Rajneesh" },
comments: [
{ id: 1, text: "Great!" },
{ id: 2, text: "Nice!" }
]
},
{
id: 2,
title: "Post 2",
author: { id: 1, name: "Rajneesh" }, // Duplicate!
comments: []
}
]
};
// Problem: To update author name, you need to update multiple places!
const state = {
posts: {
ids: [1, 2],
entities: {
1: { id: 1, title: "Post 1", authorId: 1, commentIds: [1, 2] },
2: { id: 2, title: "Post 2", authorId: 1, commentIds: [] }
}
},
authors: {
ids: [1],
entities: {
1: { id: 1, name: "Rajneesh" }
}
},
comments: {
ids: [1, 2],
entities: {
1: { id: 1, text: "Great!" },
2: { id: 2, text: "Nice!" }
}
}
};
// Benefit: Update author once, reflects everywhere!
// ========== postsSlice.js with Entity Adapter ==========
import { createSlice, createEntityAdapter, createAsyncThunk } from '@reduxjs/toolkit';
import axios from 'axios';
// Create entity adapter
const postsAdapter = createEntityAdapter({
// Assume IDs are stored in 'id' field (default)
selectId: (post) => post.id,
// Sort by title
sortComparer: (a, b) => a.title.localeCompare(b.title)
});
// Get initial state from adapter
const initialState = postsAdapter.getInitialState({
loading: false,
error: null
});
// Async thunks
export const fetchPosts = createAsyncThunk('posts/fetch', async () => {
const response = await axios.get('/api/posts');
return response.data;
});
export const addPost = createAsyncThunk('posts/add', async (post) => {
const response = await axios.post('/api/posts', post);
return response.data;
});
const postsSlice = createSlice({
name: 'posts',
initialState,
reducers: {
// Use adapter methods
postAdded: postsAdapter.addOne,
postUpdated: postsAdapter.updateOne,
postRemoved: postsAdapter.removeOne,
postsReceived: postsAdapter.setAll
},
extraReducers: (builder) => {
builder
.addCase(fetchPosts.pending, (state) => {
state.loading = true;
})
.addCase(fetchPosts.fulfilled, (state, action) => {
state.loading = false;
postsAdapter.setAll(state, action.payload);
})
.addCase(addPost.fulfilled, (state, action) => {
postsAdapter.addOne(state, action.payload);
});
}
});
// Export adapter selectors
export const {
selectAll: selectAllPosts,
selectById: selectPostById,
selectIds: selectPostIds
} = postsAdapter.getSelectors((state) => state.posts);
export const { postAdded, postUpdated, postRemoved } = postsSlice.actions;
export default postsSlice.reducer;
// Usage in component:
// const posts = useSelector(selectAllPosts);
// const post = useSelector(state => selectPostById(state, postId));
// ========== Memoized Selectors ==========
import { createSelector } from '@reduxjs/toolkit';
// Basic selectors (input selectors)
const selectTodos = (state) => state.todos.items;
const selectFilter = (state) => state.todos.filter;
const selectSearchTerm = (state) => state.todos.searchTerm;
// Memoized selector - only recalculates when todos or filter changes
export const selectFilteredTodos = createSelector(
[selectTodos, selectFilter],
(todos, filter) => {
console.log('Calculating filtered todos...'); // Only logs when recalculating
switch(filter) {
case 'active':
return todos.filter(t => !t.completed);
case 'completed':
return todos.filter(t => t.completed);
default:
return todos;
}
}
);
// Complex memoized selector with multiple inputs
export const selectSearchedAndFilteredTodos = createSelector(
[selectFilteredTodos, selectSearchTerm],
(filteredTodos, searchTerm) => {
if (!searchTerm) return filteredTodos;
return filteredTodos.filter(todo =>
todo.text.toLowerCase().includes(searchTerm.toLowerCase())
);
}
);
// Selector with parameters
export const makeSelectTodosByUser = () => {
return createSelector(
[selectTodos, (state, userId) => userId],
(todos, userId) => todos.filter(t => t.userId === userId)
);
};
// Usage in component:
function TodoList() {
const filteredTodos = useSelector(selectFilteredTodos);
// Component only re-renders when filteredTodos actually changes
return (
// ========== Custom Middleware Examples ==========
// 1. Logger Middleware
const loggerMiddleware = (store) => (next) => (action) => {
console.group(action.type);
console.log('Previous State:', store.getState());
console.log('Action:', action);
const result = next(action);
console.log('Next State:', store.getState());
console.groupEnd();
return result;
};
// 2. Error Handler Middleware
const errorMiddleware = (store) => (next) => (action) => {
try {
return next(action);
} catch (error) {
console.error('Error in action:', action.type, error);
// Send to error tracking service
// Sentry.captureException(error);
throw error;
}
};
// 3. API Middleware
const apiMiddleware = (store) => (next) => (action) => {
if (action.type !== 'API_REQUEST') {
return next(action);
}
const { url, method, data, onSuccess, onError } = action.payload;
fetch(url, { method, body: JSON.stringify(data) })
.then(res => res.json())
.then(data => store.dispatch({ type: onSuccess, payload: data }))
.catch(error => store.dispatch({ type: onError, payload: error }));
};
// 4. Local Storage Middleware (Persistence)
const localStorageMiddleware = (store) => (next) => (action) => {
const result = next(action);
// Save specific slices to localStorage
const state = store.getState();
localStorage.setItem('reduxState', JSON.stringify({
user: state.user,
cart: state.cart
}));
return result;
};
// Add middleware to store
import { configureStore } from '@reduxjs/toolkit';
const store = configureStore({
reducer: rootReducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(
loggerMiddleware,
errorMiddleware,
apiMiddleware,
localStorageMiddleware
)
});
my-redux-app/ ├── public/ │ └── index.html ├── src/ │ ├── app/ │ │ ├── store.js # Redux store configuration │ │ └── hooks.js # Custom hooks (useAppDispatch, useAppSelector) │ ├── features/ │ │ ├── auth/ │ │ │ ├── authSlice.js # Auth state logic │ │ │ ├── authAPI.js # Auth API calls │ │ │ ├── Login.jsx # Login component │ │ │ ├── Register.jsx # Register component │ │ │ └── ProtectedRoute.jsx # Protected route wrapper │ │ ├── todos/ │ │ │ ├── todosSlice.js │ │ │ ├── todosAPI.js │ │ │ ├── TodoList.jsx │ │ │ ├── TodoItem.jsx │ │ │ └── AddTodo.jsx │ │ ├── users/ │ │ │ ├── usersSlice.js │ │ │ ├── usersAPI.js │ │ │ ├── UserList.jsx │ │ │ └── UserProfile.jsx │ │ └── cart/ │ │ ├── cartSlice.js │ │ ├── Cart.jsx │ │ └── CartItem.jsx │ ├── components/ │ │ ├── common/ │ │ │ ├── Button.jsx │ │ │ ├── Input.jsx │ │ │ ├── Modal.jsx │ │ │ └── Loader.jsx │ │ └── layout/ │ │ ├── Header.jsx │ │ ├── Footer.jsx │ │ └── Sidebar.jsx │ ├── utils/ │ │ ├── api.js # Axios instance │ │ ├── constants.js # App constants │ │ └── helpers.js # Helper functions │ ├── styles/ │ │ ├── global.css │ │ └── variables.css │ ├── App.jsx │ ├── main.jsx │ └── index.css ├── package.json └── vite.config.js
// ========== app/store.js ==========
import { configureStore } from '@reduxjs/toolkit';
import authReducer from '../features/auth/authSlice';
import todosReducer from '../features/todos/todosSlice';
import usersReducer from '../features/users/usersSlice';
import cartReducer from '../features/cart/cartSlice';
export const store = configureStore({
reducer: {
auth: authReducer,
todos: todosReducer,
users: usersReducer,
cart: cartReducer
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: false
})
});
export type RootState = ReturnType;
export type AppDispatch = typeof store.dispatch;
// ========== app/hooks.js ==========
import { useDispatch, useSelector } from 'react-redux';
// Custom typed hooks
export const useAppDispatch = () => useDispatch();
export const useAppSelector = useSelector;
// Custom hook for async actions with loading state
export const useAsyncAction = () => {
const dispatch = useAppDispatch();
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const execute = async (asyncAction) => {
setLoading(true);
setError(null);
try {
await dispatch(asyncAction).unwrap();
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
return { execute, loading, error };
};
Definition: Ducks pattern keeps all Redux logic for a feature in a single file.
// ========== features/todos/todosDuck.js ==========
// All in one file: actions, reducers, selectors, thunks
import { createSlice, createAsyncThunk, createSelector } from '@reduxjs/toolkit';
import * as api from './todosAPI';
// ===== ASYNC THUNKS =====
export const fetchTodos = createAsyncThunk('todos/fetch', api.fetchTodos);
export const addTodo = createAsyncThunk('todos/add', api.addTodo);
export const updateTodo = createAsyncThunk('todos/update', api.updateTodo);
export const deleteTodo = createAsyncThunk('todos/delete', api.deleteTodo);
// ===== SLICE =====
const todosSlice = createSlice({
name: 'todos',
initialState: {
items: [],
loading: false,
error: null,
filter: 'all'
},
reducers: {
setFilter: (state, action) => {
state.filter = action.payload;
},
clearError: (state) => {
state.error = null;
}
},
extraReducers: (builder) => {
builder
.addCase(fetchTodos.pending, (state) => {
state.loading = true;
})
.addCase(fetchTodos.fulfilled, (state, action) => {
state.loading = false;
state.items = action.payload;
})
.addCase(fetchTodos.rejected, (state, action) => {
state.loading = false;
state.error = action.error.message;
});
}
});
// ===== ACTIONS =====
export const { setFilter, clearError } = todosSlice.actions;
// ===== SELECTORS =====
const selectTodosState = (state) => state.todos;
export const selectAllTodos = (state) => state.todos.items;
export const selectTodosLoading = (state) => state.todos.loading;
export const selectTodosError = (state) => state.todos.error;
export const selectFilter = (state) => state.todos.filter;
export const selectFilteredTodos = createSelector(
[selectAllTodos, selectFilter],
(todos, filter) => {
switch(filter) {
case 'active': return todos.filter(t => !t.completed);
case 'completed': return todos.filter(t => t.completed);
default: return todos;
}
}
);
// ===== REDUCER =====
export default todosSlice.reducer;
Hinglish: Redux ek state management library hai jo application ka saara data ek jagah store karta hai. Jab bahut saare components ko same data chahiye hota hai, tab Redux use karte hain taaki prop drilling se bach sakein.
Key Points:
Hinglish: Redux ke 4 main parts hain - Store (jahan data rakha jata hai), Actions (kya karna hai ye batata hai), Reducers (data ko update karta hai), aur Dispatch (action ko bhejta hai).
Hinglish: RTK mein kam code likhna padta hai, setup easy hai, aur built-in features jaise Immer aur Thunk already included hain. Old Redux mein bahut zyada boilerplate code likhna padta tha.
Hinglish: useSelector store se data read karne ke liye use hota hai, aur useDispatch actions bhejne ke liye use hota hai. Ye dono React-Redux ke hooks hain.
import { useSelector, useDispatch } from 'react-redux';
import { increment } from './counterSlice';
function Counter() {
// Read data from store
const count = useSelector(state => state.counter.value);
// Get dispatch function
const dispatch = useDispatch();
return (
Count: {count}
);
}
Hinglish: Pure function isliye chahiye kyunki same input pe hamesha same output milna chahiye. Isse Redux predictable rehta hai aur debugging easy ho jati hai. Side effects nahi hone chahiye reducer mein.
// ✅ Pure Function (Good)
function counterReducer(state = 0, action) {
if (action.type === 'INCREMENT') {
return state + 1; // Returns new value
}
return state;
}
// ❌ Impure Function (Bad)
function badReducer(state = 0, action) {
if (action.type === 'INCREMENT') {
state++; // Mutates state directly
fetch('/api/log'); // Side effect
return state + Math.random(); // Non-deterministic
}
return state;
}
Hinglish: createSlice ek function hai jo automatically actions aur reducer dono bana deta hai. Isme hum sirf reducers define karte hain aur actions automatically generate ho jate hain.
Hinglish: Middleware ek layer hai jo action dispatch hone aur reducer tak pahunchne ke beech mein kaam karta hai. Iska use logging, async operations, ya error handling ke liye hota hai.
Hinglish: Jab API call karni ho ya koi async operation karna ho tab createAsyncThunk use karte hain. Ye automatically pending, fulfilled, aur rejected states handle kar leta hai.
Features: Add, Edit, Delete, Toggle, Filter todos with Redux Toolkit
todo-app/ ├── src/ │ ├── app/ │ │ └── store.js │ ├── features/ │ │ └── todos/ │ │ ├── todosSlice.js │ │ └── TodoList.jsx │ ├── App.jsx │ └── main.jsx └── package.json
Features: Add to cart, Remove, Update quantity, Calculate total, Apply discounts
// ========== REDUX TOOLKIT CHEAT SHEET ==========
// 1. INSTALL
npm install @reduxjs/toolkit react-redux
// 2. CREATE STORE
import { configureStore } from '@reduxjs/toolkit';
const store = configureStore({
reducer: { counter: counterReducer }
});
// 3. CREATE SLICE
import { createSlice } from '@reduxjs/toolkit';
const slice = createSlice({
name: 'counter',
initialState: { value: 0 },
reducers: {
increment: state => { state.value += 1; }
}
});
// 4. ASYNC THUNK
import { createAsyncThunk } from '@reduxjs/toolkit';
const fetchData = createAsyncThunk('data/fetch', async () => {
const response = await fetch('/api/data');
return response.json();
});
// 5. USE IN COMPONENT
import { useSelector, useDispatch } from 'react-redux';
const value = useSelector(state => state.counter.value);
const dispatch = useDispatch();
dispatch(increment());
// 6. SELECTORS
import { createSelector } from '@reduxjs/toolkit';
const selectFiltered = createSelector(
[selectItems, selectFilter],
(items, filter) => items.filter(i => i.type === filter)
);