State Management with Redux

Master Redux for managing complex application state.

Understanding State Management: Why Redux?

As your React application grows, managing state becomes increasingly complex. You might find yourself passing props through many component levels (prop drilling) or struggling to share state between unrelated components. This is where Redux comes in.

What is Redux? Redux is like a central bank for your application's data. Instead of each component managing its own money (state) in its pocket, all the money is stored in one secure vault (the Redux store). Components can deposit, withdraw, or check their balance through official procedures.

The State Management Problem Imagine building a social media app:

  • User info needed in header, profile, posts, and settings
  • Theme preference affects every component
  • Notifications need to appear anywhere
  • Shopping cart data accessed from multiple pages

When to Use Redux ✅ Use Redux when:

  • Multiple components need the same data
  • State needs to persist across routes
  • You need powerful debugging tools
  • Application has complex state logic

❌ Don't use Redux when:

  • Building a simple app with few components
  • State is only used locally
  • Props passing is only 1-2 levels deep
1// Without Redux - Prop Drilling Problem
2function App() {
3 const [user, setUser] = useState({ name: 'John' });
4
5 return (
6 <Layout user={user}>
7 <Header user={user} />
8 <MainContent user={user} setUser={setUser} />
9 </Layout>
10 );
11}
12
13// With Redux - Clean Component Tree
14function AppWithRedux() {
15 return (
16 <Provider store={store}>
17 <Layout>
18 <Header /> {/* Gets user from Redux */}
19 <MainContent /> {/* No props needed! */}
20 </Layout>
21 </Provider>
22 );
23}
24
25// Redux Flow: Action → Dispatch → Reducer → Store → UI
26
27// 1. Actions - describe what happened
28const loginAction = {
29 type: 'user/login',
30 payload: { id: 1, name: 'John' }
31};
32
33// 2. Reducer - how to update state
34function userReducer(state = null, action) {
35 switch (action.type) {
36 case 'user/login':
37 return action.payload;
38 case 'user/logout':
39 return null;
40 default:
41 return state;
42 }
43}
44
45// 3. Using Redux in Components
46function Header() {
47 const user = useSelector(state => state.user);
48 const dispatch = useDispatch();
49
50 return (
51 <div>
52 <span>Welcome, {user?.name || 'Guest'}</span>
53 <button onClick={() => dispatch({ type: 'user/logout' })}>
54 Logout
55 </button>
56 </div>
57 );
58}

Redux Fundamentals: Core Concepts Step by Step

Redux is a predictable state container for JavaScript applications. It helps you manage application state in a centralized store and makes state changes predictable and debuggable.

Core Principles:

  • Single Source of Truth: The global state is stored in a single store
  • State is Read-Only: State can only be changed by dispatching actions
  • Changes are Made with Pure Functions: Reducers specify how state changes

Understanding Each Principle

  1. Single Source of Truth: All application state lives in one object

    • Easy to debug and inspect
    • Enables powerful developer tools
    • Makes server rendering possible
  2. State is Read-Only: Components can't directly modify state

    • All changes go through a formal process
    • Makes state changes trackable
    • Prevents accidental mutations
  3. Pure Functions (Reducers): Given the same input, always return same output

    • No side effects (API calls, random values)
    • Easy to test and predict
    • Enable time-travel debugging
1// Step 1: Install Redux Toolkit
2// npm install @reduxjs/toolkit react-redux
3
4// Step 2: Create the Store
5import { configureStore } from '@reduxjs/toolkit';
6import counterReducer from './features/counterSlice';
7import userReducer from './features/userSlice';
8import cartReducer from './features/cartSlice';
9
10export const store = configureStore({
11 reducer: {
12 counter: counterReducer,
13 user: userReducer,
14 cart: cartReducer,
15 },
16});
17
18// TypeScript types
19export type RootState = ReturnType<typeof store.getState>;
20export type AppDispatch = typeof store.dispatch;
21
22// Step 3: Provide the Store to Your App
23import React from 'react';
24import ReactDOM from 'react-dom';
25import { Provider } from 'react-redux';
26import { store } from './app/store';
27import App from './App';
28
29ReactDOM.render(
30 <Provider store={store}>
31 <App />
32 </Provider>,
33 document.getElementById('root')
34);
35
36// Step 4: Access Redux State in Components
37import { useSelector, useDispatch } from 'react-redux';
38
39function MyComponent() {
40 const count = useSelector((state) => state.counter.value);
41 const user = useSelector((state) => state.user.currentUser);
42 const dispatch = useDispatch();
43
44 return (
45 <div>
46 <h2>Count: {count}</h2>
47 <p>Logged in as: {user?.name || 'Guest'}</p>
48 <button onClick={() => dispatch(increment())}>
49 Increment
50 </button>
51 </div>
52 );
53}
54
55// Understanding the Redux Flow with a Real Example
56
57// 1. User clicks "Add to Cart" button
58function ProductCard({ product }) {
59 const dispatch = useDispatch();
60
61 const handleAddToCart = () => {
62 dispatch({
63 type: 'cart/addItem',
64 payload: product
65 });
66 };
67
68 return (
69 <div>
70 <h3>{product.name}</h3>
71 <button onClick={handleAddToCart}>Add to Cart</button>
72 </div>
73 );
74}
75
76// 2. Reducer updates state
77function cartReducer(state = { items: [] }, action) {
78 switch (action.type) {
79 case 'cart/addItem':
80 return {
81 ...state,
82 items: [...state.items, action.payload]
83 };
84 default:
85 return state;
86 }
87}
88
89// 3. Components re-render
90function CartIcon() {
91 const itemCount = useSelector(state => state.cart.items.length);
92
93 return (
94 <div>
95 🛒 ({itemCount})
96 </div>
97 );
98}
99
100// Redux DevTools State Example
101/*
102State Tree:
103{
104 counter: { value: 42, step: 1 },
105 user: { currentUser: { id: 1, name: "John" }, isLoading: false },
106 cart: {
107 items: [
108 { id: 1, name: "iPhone", price: 999 },
109 { id: 2, name: "MacBook", price: 1999 }
110 ],
111 total: 2998
112 }
113}
114
115Action Log:
116- cart/addItem { payload: { id: 1, name: "iPhone" } }
117- user/login { payload: { id: 1, name: "John" } }
118- counter/increment
119*/

Creating Slices with Redux Toolkit: The Modern Way

Redux Toolkit's createSlice function simplifies the process of writing reducers and action creators. It uses Immer under the hood, allowing you to write "mutative" logic that is actually immutable.

What is a Slice? A "slice" is a collection of Redux reducer logic and actions for a single feature of your app. For example, a blog might have separate slices for posts, comments, and users.

Benefits of createSlice:

  1. Less Boilerplate: No need to write action types and creators separately
  2. Immer Integration: Write "mutating" logic that's actually immutable
  3. Type Safety: Automatic TypeScript types
  4. Better Developer Experience: Clear, concise code

Slice Structure:

  • name: Identifies the slice
  • initialState: Starting state value
  • reducers: Functions that handle state updates
  • extraReducers: Handle actions from other slices
1// 🍰 CREATING YOUR FIRST SLICE - COUNTER EXAMPLE
2
3// counterSlice.js
4import { createSlice, PayloadAction } from '@reduxjs/toolkit';
5
6// Define the shape of your state
7interface CounterState {
8 value: number;
9 step: number;
10 history: number[];
11}
12
13// Set initial state
14const initialState: CounterState = {
15 value: 0,
16 step: 1,
17 history: [],
18};
19
20// Create the slice
21export const counterSlice = createSlice({
22 name: 'counter', // Slice name
23 initialState, // Initial state
24 reducers: { // Reducer functions
25 // Action creators are generated automatically!
26 increment: (state) => {
27 // Thanks to Immer, we can "mutate" state directly
28 state.value += state.step;
29 state.history.push(state.value);
30 },
31
32 decrement: (state) => {
33 state.value -= state.step;
34 state.history.push(state.value);
35 },
36
37 incrementByAmount: (state, action: PayloadAction<number>) => {
38 state.value += action.payload;
39 state.history.push(state.value);
40 },
41
42 setStep: (state, action: PayloadAction<number>) => {
43 state.step = action.payload;
44 },
45
46 reset: (state) => {
47 // Can also return a new state object
48 return initialState;
49 },
50
51 undo: (state) => {
52 if (state.history.length > 1) {
53 state.history.pop(); // Remove current value
54 state.value = state.history[state.history.length - 1] || 0;
55 }
56 },
57 },
58});
59
60// Export actions (auto-generated!)
61export const {
62 increment,
63 decrement,
64 incrementByAmount,
65 setStep,
66 reset,
67 undo
68} = counterSlice.actions;
69
70// Export reducer
71export default counterSlice.reducer;
72
73// 🎮 USING THE COUNTER SLICE IN A COMPONENT
74
75import React from 'react';
76import { useSelector, useDispatch } from 'react-redux';
77import { RootState } from './store';
78import {
79 increment,
80 decrement,
81 incrementByAmount,
82 setStep,
83 reset,
84 undo
85} from './counterSlice';
86
87function Counter() {
88 // Select state from store
89 const count = useSelector((state: RootState) => state.counter.value);
90 const step = useSelector((state: RootState) => state.counter.step);
91 const history = useSelector((state: RootState) => state.counter.history);
92
93 // Get dispatch function
94 const dispatch = useDispatch();
95
96 // Local state for custom amount input
97 const [customAmount, setCustomAmount] = React.useState('10');
98
99 return (
100 <div style={{ padding: '20px', maxWidth: '400px', margin: '0 auto' }}>
101 <h2>Redux Counter Example</h2>
102
103 {/* Display current count */}
104 <div style={{
105 fontSize: '48px',
106 fontWeight: 'bold',
107 textAlign: 'center',
108 margin: '20px 0'
109 }}>
110 {count}
111 </div>
112
113 {/* Step size control */}
114 <div style={{ marginBottom: '20px' }}>
115 <label>
116 Step Size:
117 <input
118 type="number"
119 value={step}
120 onChange={(e) => dispatch(setStep(Number(e.target.value)))}
121 style={{ marginLeft: '10px', width: '60px' }}
122 min="1"
123 />
124 </label>
125 </div>
126
127 {/* Main controls */}
128 <div style={{
129 display: 'flex',
130 gap: '10px',
131 justifyContent: 'center',
132 marginBottom: '20px'
133 }}>
134 <button onClick={() => dispatch(decrement())}>
135 - {step}
136 </button>
137 <button onClick={() => dispatch(increment())}>
138 + {step}
139 </button>
140 </div>
141
142 {/* Custom increment */}
143 <div style={{ marginBottom: '20px', textAlign: 'center' }}>
144 <input
145 type="number"
146 value={customAmount}
147 onChange={(e) => setCustomAmount(e.target.value)}
148 style={{ width: '80px', marginRight: '10px' }}
149 />
150 <button onClick={() => dispatch(incrementByAmount(Number(customAmount)))}>
151 Add Amount
152 </button>
153 </div>
154
155 {/* Utility buttons */}
156 <div style={{
157 display: 'flex',
158 gap: '10px',
159 justifyContent: 'center'
160 }}>
161 <button onClick={() => dispatch(reset())}>
162 Reset
163 </button>
164 <button
165 onClick={() => dispatch(undo())}
166 disabled={history.length <= 1}
167 >
168 Undo
169 </button>
170 </div>
171
172 {/* History display */}
173 <div style={{ marginTop: '20px' }}>
174 <h4>History (last 5 values):</h4>
175 <div style={{ display: 'flex', gap: '5px', flexWrap: 'wrap' }}>
176 {history.slice(-5).map((val, idx) => (
177 <span
178 key={idx}
179 style={{
180 padding: '5px 10px',
181 background: '#f0f0f0',
182 borderRadius: '4px',
183 fontSize: '14px'
184 }}
185 >
186 {val}
187 </span>
188 ))}
189 </div>
190 </div>
191 </div>
192 );
193}
194
195// 🛍️ MORE COMPLEX SLICE EXAMPLE - SHOPPING CART
196
197interface CartItem {
198 id: number;
199 name: string;
200 price: number;
201 quantity: number;
202}
203
204interface CartState {
205 items: CartItem[];
206 total: number;
207 discount: number;
208}
209
210const cartSlice = createSlice({
211 name: 'cart',
212 initialState: {
213 items: [],
214 total: 0,
215 discount: 0,
216 } as CartState,
217 reducers: {
218 addItem: (state, action: PayloadAction<Omit<CartItem, 'quantity'>>) => {
219 const existingItem = state.items.find(item => item.id === action.payload.id);
220
221 if (existingItem) {
222 existingItem.quantity += 1;
223 } else {
224 state.items.push({ ...action.payload, quantity: 1 });
225 }
226
227 // Recalculate total
228 state.total = state.items.reduce(
229 (sum, item) => sum + (item.price * item.quantity),
230 0
231 );
232 },
233
234 removeItem: (state, action: PayloadAction<number>) => {
235 state.items = state.items.filter(item => item.id !== action.payload);
236 state.total = state.items.reduce(
237 (sum, item) => sum + (item.price * item.quantity),
238 0
239 );
240 },
241
242 updateQuantity: (state, action: PayloadAction<{ id: number; quantity: number }>) => {
243 const item = state.items.find(item => item.id === action.payload.id);
244 if (item) {
245 item.quantity = action.payload.quantity;
246 if (item.quantity <= 0) {
247 state.items = state.items.filter(i => i.id !== item.id);
248 }
249 }
250 state.total = state.items.reduce(
251 (sum, item) => sum + (item.price * item.quantity),
252 0
253 );
254 },
255
256 applyDiscount: (state, action: PayloadAction<number>) => {
257 state.discount = action.payload;
258 },
259
260 clearCart: (state) => {
261 return { items: [], total: 0, discount: 0 };
262 },
263 },
264});
265
266// 💡 SLICE BEST PRACTICES
267
268// 1. Keep slices focused on one feature
269// ❌ Bad: One giant slice for everything
270const badSlice = createSlice({
271 name: 'app',
272 initialState: {
273 user: null,
274 posts: [],
275 comments: [],
276 ui: {},
277 // Too much in one slice!
278 },
279 reducers: {
280 // Hundreds of reducers...
281 }
282});
283
284// ✅ Good: Separate slices for each feature
285const userSlice = createSlice({ name: 'user', /* ... */ });
286const postsSlice = createSlice({ name: 'posts', /* ... */ });
287const commentsSlice = createSlice({ name: 'comments', /* ... */ });
288
289// 2. Name actions clearly
290// ❌ Bad: Vague action names
291const vagueSice = createSlice({
292 name: 'data',
293 reducers: {
294 update: (state, action) => { /* What are we updating? */ },
295 set: (state, action) => { /* Set what? */ },
296 }
297});
298
299// ✅ Good: Descriptive action names
300const descriptiveSlice = createSlice({
301 name: 'user',
302 reducers: {
303 setUserProfile: (state, action) => { /* Clear! */ },
304 updateUserEmail: (state, action) => { /* Specific! */ },
305 }
306});

Async Actions with createAsyncThunk: Handling API Calls

Redux Toolkit provides createAsyncThunk for handling asynchronous operations like API calls. It automatically generates action types and action creators for pending, fulfilled, and rejected states.

Why createAsyncThunk? Async operations have three states:

  1. Pending: Request in progress
  2. Fulfilled: Request succeeded
  3. Rejected: Request failed

createAsyncThunk handles all these states automatically, saving you from writing boilerplate code.

How It Works:

  1. You define an async function
  2. Redux Toolkit creates action types for each state
  3. Your reducer handles these actions
  4. Components can track loading/error states

Benefits:

  • Automatic loading states
  • Built-in error handling
  • Cancelable requests
  • TypeScript support
  • Works with Redux DevTools
1// 🚀 ASYNC ACTIONS WITH createAsyncThunk
2
3import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
4
5// Define types
6interface Post {
7 id: number;
8 title: string;
9 body: string;
10 userId: number;
11}
12
13interface PostState {
14 posts: Post[];
15 currentPost: Post | null;
16 loading: boolean;
17 error: string | null;
18}
19
20// Initial state
21const initialState: PostState = {
22 posts: [],
23 currentPost: null,
24 loading: false,
25 error: null,
26};
27
28// 🎯 CREATE ASYNC THUNK - Fetch posts from API
29export const fetchPosts = createAsyncThunk(
30 'posts/fetchPosts',
31 async () => {
32 const response = await fetch('https://jsonplaceholder.typicode.com/posts?_limit=5');
33 if (!response.ok) throw new Error('Failed to fetch');
34 return response.json();
35 }
36);
37
38// Fetch single post
39export const fetchPostById = createAsyncThunk(
40 'posts/fetchById',
41 async (postId: number) => {
42 const response = await fetch(`https://jsonplaceholder.typicode.com/posts/${postId}`);
43 if (!response.ok) throw new Error('Post not found');
44 return response.json();
45 }
46);
47
48// 🍰 CREATE THE SLICE
49const postSlice = createSlice({
50 name: 'posts',
51 initialState,
52 reducers: {
53 // Regular actions
54 clearCurrentPost: (state) => {
55 state.currentPost = null;
56 },
57 },
58 // Handle async actions
59 extraReducers: (builder) => {
60 builder
61 // Fetch posts cases
62 .addCase(fetchPosts.pending, (state) => {
63 state.loading = true;
64 state.error = null;
65 })
66 .addCase(fetchPosts.fulfilled, (state, action) => {
67 state.loading = false;
68 state.posts = action.payload;
69 })
70 .addCase(fetchPosts.rejected, (state, action) => {
71 state.loading = false;
72 state.error = action.error.message || 'Failed to fetch posts';
73 })
74 // Fetch single post
75 .addCase(fetchPostById.fulfilled, (state, action) => {
76 state.currentPost = action.payload;
77 });
78 },
79});
80
81export const { clearCurrentPost } = postSlice.actions;
82export default postSlice.reducer;
83
84// 🎮 USING IN A COMPONENT
85function PostList() {
86 const dispatch = useDispatch();
87 const { posts, loading, error } = useSelector(state => state.posts);
88
89 useEffect(() => {
90 dispatch(fetchPosts());
91 }, [dispatch]);
92
93 if (loading) return <div>Loading posts...</div>;
94 if (error) return <div>Error: {error}</div>;
95
96 return (
97 <div>
98 <h2>Blog Posts</h2>
99 {posts.map(post => (
100 <div key={post.id} onClick={() => dispatch(fetchPostById(post.id))}>
101 <h3>{post.title}</h3>
102 <p>{post.body.substring(0, 100)}...</p>
103 </div>
104 ))}
105 </div>
106 );
107}
108
109// 💡 REAL-WORLD EXAMPLE: Authentication
110interface AuthState {
111 user: { id: string; name: string; email: string } | null;
112 token: string | null;
113 loading: boolean;
114 error: string | null;
115}
116
117// Login async thunk
118export const login = createAsyncThunk(
119 'auth/login',
120 async ({ email, password }: { email: string; password: string }) => {
121 const response = await fetch('/api/login', {
122 method: 'POST',
123 headers: { 'Content-Type': 'application/json' },
124 body: JSON.stringify({ email, password }),
125 });
126
127 if (!response.ok) {
128 const error = await response.json();
129 throw new Error(error.message || 'Login failed');
130 }
131
132 return response.json(); // { user, token }
133 }
134);
135
136// Auth slice
137const authSlice = createSlice({
138 name: 'auth',
139 initialState: {
140 user: null,
141 token: null,
142 loading: false,
143 error: null,
144 } as AuthState,
145 reducers: {
146 logout: (state) => {
147 state.user = null;
148 state.token = null;
149 localStorage.removeItem('token');
150 },
151 },
152 extraReducers: (builder) => {
153 builder
154 .addCase(login.pending, (state) => {
155 state.loading = true;
156 state.error = null;
157 })
158 .addCase(login.fulfilled, (state, action) => {
159 state.loading = false;
160 state.user = action.payload.user;
161 state.token = action.payload.token;
162 localStorage.setItem('token', action.payload.token);
163 })
164 .addCase(login.rejected, (state, action) => {
165 state.loading = false;
166 state.error = action.error.message || 'Login failed';
167 });
168 },
169});
170
171// Login component
172function LoginForm() {
173 const dispatch = useDispatch();
174 const { loading, error } = useSelector(state => state.auth);
175 const [email, setEmail] = useState('');
176 const [password, setPassword] = useState('');
177
178 const handleSubmit = async (e) => {
179 e.preventDefault();
180 const result = await dispatch(login({ email, password }));
181
182 if (login.fulfilled.match(result)) {
183 // Navigate to dashboard
184 console.log('Login successful!');
185 }
186 };
187
188 return (
189 <form onSubmit={handleSubmit}>
190 {error && <div className="error">{error}</div>}
191
192 <input
193 type="email"
194 value={email}
195 onChange={(e) => setEmail(e.target.value)}
196 placeholder="Email"
197 required
198 />
199
200 <input
201 type="password"
202 value={password}
203 onChange={(e) => setPassword(e.target.value)}
204 placeholder="Password"
205 required
206 />
207
208 <button type="submit" disabled={loading}>
209 {loading ? 'Logging in...' : 'Login'}
210 </button>
211 </form>
212 );
213}

Redux DevTools and Best Practices

Redux DevTools Extension provides powerful debugging capabilities, and middleware allows you to extend Redux with custom functionality like logging, crash reporting, or handling async actions.

Redux DevTools Features:

  1. Action History: See every action dispatched
  2. State Diff: Compare state before/after actions
  3. Time Travel: Jump to any previous state
  4. Import/Export: Save and load state
  5. Action Replay: Re-run a sequence of actions

Middleware: Middleware sits between dispatching an action and the reducer. It can:

  • Log actions and state
  • Report crashes
  • Handle async actions
  • Transform actions
  • Stop certain actions

Best Practices:

  1. Normalize State Shape: Keep state flat and avoid nested data
  2. Use Selectors: Compute derived data with memoization
  3. Keep Logic in Reducers: Business logic belongs in reducers, not components
  4. Use TypeScript: Get type safety and better IDE support
  5. Split Code: Organize by feature, not by file type
1// 🛠️ ENHANCED STORE CONFIGURATION
2
3import { configureStore, getDefaultMiddleware } from '@reduxjs/toolkit';
4import { createLogger } from 'redux-logger';
5import counterReducer from './features/counterSlice';
6import userReducer from './features/userSlice';
7import { api } from './services/api';
8
9// Custom middleware for crash reporting
10const crashReporter = (store) => (next) => (action) => {
11 try {
12 return next(action);
13 } catch (err) {
14 console.error('Caught an exception!', err);
15 console.error('Action:', action);
16 console.error('State:', store.getState());
17
18 // Send to crash reporting service
19 if (window.Sentry) {
20 window.Sentry.captureException(err, {
21 extra: {
22 action,
23 state: store.getState()
24 }
25 });
26 }
27
28 throw err;
29 }
30};
31
32// Custom middleware for analytics
33const analytics = (store) => (next) => (action) => {
34 // Track specific actions
35 if (action.type === 'cart/checkout') {
36 window.gtag?.('event', 'purchase', {
37 value: store.getState().cart.total,
38 items: store.getState().cart.items.length
39 });
40 }
41
42 return next(action);
43};
44
45// Logger middleware (only in development)
46const logger = createLogger({
47 predicate: () => process.env.NODE_ENV === 'development',
48 collapsed: true,
49 duration: true,
50 diff: true,
51});
52
53// Configure store with middleware
54export const store = configureStore({
55 reducer: {
56 counter: counterReducer,
57 user: userReducer,
58 [api.reducerPath]: api.reducer, // RTK Query
59 },
60 middleware: (getDefaultMiddleware) =>
61 getDefaultMiddleware({
62 serializableCheck: {
63 // Ignore these action types
64 ignoredActions: ['persist/PERSIST'],
65 // Ignore these paths in state
66 ignoredPaths: ['user.lastSeen'],
67 },
68 thunk: {
69 extraArgument: {
70 api: 'https://api.example.com',
71 },
72 },
73 })
74 .concat(api.middleware) // RTK Query
75 .concat(crashReporter)
76 .concat(analytics)
77 .concat(logger),
78 devTools: process.env.NODE_ENV !== 'production' && {
79 name: 'MyApp',
80 trace: true,
81 traceLimit: 25,
82 },
83});
84
85// 🎯 TYPED HOOKS
86
87import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
88
89export type RootState = ReturnType<typeof store.getState>;
90export type AppDispatch = typeof store.dispatch;
91
92// Use these instead of plain useDispatch and useSelector
93export const useAppDispatch = () => useDispatch<AppDispatch>();
94export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
95
96// 📊 SELECTORS WITH RESELECT
97
98import { createSelector } from '@reduxjs/toolkit';
99
100// Basic selectors (input selectors)
101const selectUsers = (state: RootState) => state.user.users;
102const selectCurrentUserId = (state: RootState) => state.user.currentUser?.id;
103const selectCartItems = (state: RootState) => state.cart.items;
104const selectCartDiscount = (state: RootState) => state.cart.discount;
105
106// Memoized selectors (only recalculate when inputs change)
107export const selectCurrentUser = createSelector(
108 [selectUsers, selectCurrentUserId],
109 (users, userId) => users.find(user => user.id === userId)
110);
111
112export const selectCartTotal = createSelector(
113 [selectCartItems, selectCartDiscount],
114 (items, discount) => {
115 const subtotal = items.reduce(
116 (sum, item) => sum + (item.price * item.quantity),
117 0
118 );
119 return subtotal * (1 - discount);
120 }
121);
122
123export const selectCartItemCount = createSelector(
124 [selectCartItems],
125 (items) => items.reduce((count, item) => count + item.quantity, 0)
126);
127
128export const selectExpensiveItems = createSelector(
129 [selectCartItems],
130 (items) => items.filter(item => item.price > 100)
131);
132
133// Parameterized selector
134export const makeSelectUserById = () =>
135 createSelector(
136 [selectUsers, (state: RootState, userId: number) => userId],
137 (users, userId) => users.find(user => user.id === userId)
138 );
139
140// Using selectors in components
141function CartSummary() {
142 const total = useAppSelector(selectCartTotal);
143 const itemCount = useAppSelector(selectCartItemCount);
144 const expensiveItems = useAppSelector(selectExpensiveItems);
145
146 return (
147 <div>
148 <h3>Cart Summary</h3>
149 <p>Items: {itemCount}</p>
150 <p>Total: ${total.toFixed(2)}</p>
151 <p>Expensive items: {expensiveItems.length}</p>
152 </div>
153 );
154}
155
156// 🏗️ STATE NORMALIZATION
157
158// ❌ Bad: Deeply nested state
159const badState = {
160 posts: [
161 {
162 id: 1,
163 title: 'Post 1',
164 author: {
165 id: 1,
166 name: 'John',
167 posts: [/* circular reference! */]
168 },
169 comments: [
170 {
171 id: 1,
172 text: 'Great post!',
173 author: { id: 2, name: 'Jane' }
174 }
175 ]
176 }
177 ]
178};
179
180// ✅ Good: Normalized state
181const goodState = {
182 posts: {
183 byId: {
184 1: { id: 1, title: 'Post 1', authorId: 1, commentIds: [1] }
185 },
186 allIds: [1]
187 },
188 users: {
189 byId: {
190 1: { id: 1, name: 'John' },
191 2: { id: 2, name: 'Jane' }
192 },
193 allIds: [1, 2]
194 },
195 comments: {
196 byId: {
197 1: { id: 1, text: 'Great post!', authorId: 2, postId: 1 }
198 },
199 allIds: [1]
200 }
201};
202
203// Entity adapter for normalization
204import { createEntityAdapter } from '@reduxjs/toolkit';
205
206const usersAdapter = createEntityAdapter<User>({
207 selectId: (user) => user.id,
208 sortComparer: (a, b) => a.name.localeCompare(b.name),
209});
210
211const userSlice = createSlice({
212 name: 'users',
213 initialState: usersAdapter.getInitialState({
214 loading: false,
215 error: null,
216 }),
217 reducers: {
218 addUser: usersAdapter.addOne,
219 addUsers: usersAdapter.addMany,
220 updateUser: usersAdapter.updateOne,
221 removeUser: usersAdapter.removeOne,
222 },
223});
224
225// 📁 FEATURE-BASED FOLDER STRUCTURE
226
227/*
228src/
229 features/
230 counter/
231 counterSlice.ts
232 Counter.tsx
233 Counter.test.tsx
234 user/
235 userSlice.ts
236 userAPI.ts
237 UserList.tsx
238 UserProfile.tsx
239 cart/
240 cartSlice.ts
241 Cart.tsx
242 CartItem.tsx
243 app/
244 store.ts
245 hooks.ts
246 common/
247 components/
248 utils/
249*/
250
251// 💡 REDUX PATTERNS AND TIPS
252
253// 1. Action creator patterns
254const userSlice = createSlice({
255 name: 'user',
256 initialState,
257 reducers: {
258 // Simple action
259 logout: (state) => {
260 state.currentUser = null;
261 },
262
263 // Action with prepare callback
264 loginSuccess: {
265 reducer: (state, action) => {
266 state.currentUser = action.payload.user;
267 state.token = action.payload.token;
268 },
269 prepare: (user, token) => ({
270 payload: { user, token, timestamp: Date.now() }
271 }),
272 },
273 },
274});
275
276// 2. Handling loading states generically
277const createAsyncSlice = (name: string) => {
278 return createSlice({
279 name,
280 initialState: {
281 data: null,
282 loading: false,
283 error: null,
284 },
285 reducers: {},
286 extraReducers: (builder) => {
287 builder
288 .addMatcher(
289 (action) => action.type.endsWith('/pending'),
290 (state) => {
291 state.loading = true;
292 state.error = null;
293 }
294 )
295 .addMatcher(
296 (action) => action.type.endsWith('/fulfilled'),
297 (state, action) => {
298 state.loading = false;
299 state.data = action.payload;
300 }
301 )
302 .addMatcher(
303 (action) => action.type.endsWith('/rejected'),
304 (state, action) => {
305 state.loading = false;
306 state.error = action.payload;
307 }
308 );
309 },
310 });
311};