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 Problem2function App() {3 const [user, setUser] = useState({ name: 'John' });45 return (6 <Layout user={user}>7 <Header user={user} />8 <MainContent user={user} setUser={setUser} />9 </Layout>10 );11}1213// With Redux - Clean Component Tree14function 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}2425// Redux Flow: Action → Dispatch → Reducer → Store → UI2627// 1. Actions - describe what happened28const loginAction = {29 type: 'user/login',30 payload: { id: 1, name: 'John' }31};3233// 2. Reducer - how to update state34function 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}4445// 3. Using Redux in Components46function Header() {47 const user = useSelector(state => state.user);48 const dispatch = useDispatch();4950 return (51 <div>52 <span>Welcome, {user?.name || 'Guest'}</span>53 <button onClick={() => dispatch({ type: 'user/logout' })}>54 Logout55 </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
-
Single Source of Truth: All application state lives in one object
- Easy to debug and inspect
- Enables powerful developer tools
- Makes server rendering possible
-
State is Read-Only: Components can't directly modify state
- All changes go through a formal process
- Makes state changes trackable
- Prevents accidental mutations
-
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 Toolkit2// npm install @reduxjs/toolkit react-redux34// Step 2: Create the Store5import { configureStore } from '@reduxjs/toolkit';6import counterReducer from './features/counterSlice';7import userReducer from './features/userSlice';8import cartReducer from './features/cartSlice';910export const store = configureStore({11 reducer: {12 counter: counterReducer,13 user: userReducer,14 cart: cartReducer,15 },16});1718// TypeScript types19export type RootState = ReturnType<typeof store.getState>;20export type AppDispatch = typeof store.dispatch;2122// Step 3: Provide the Store to Your App23import React from 'react';24import ReactDOM from 'react-dom';25import { Provider } from 'react-redux';26import { store } from './app/store';27import App from './App';2829ReactDOM.render(30 <Provider store={store}>31 <App />32 </Provider>,33 document.getElementById('root')34);3536// Step 4: Access Redux State in Components37import { useSelector, useDispatch } from 'react-redux';3839function MyComponent() {40 const count = useSelector((state) => state.counter.value);41 const user = useSelector((state) => state.user.currentUser);42 const dispatch = useDispatch();4344 return (45 <div>46 <h2>Count: {count}</h2>47 <p>Logged in as: {user?.name || 'Guest'}</p>48 <button onClick={() => dispatch(increment())}>49 Increment50 </button>51 </div>52 );53}5455// Understanding the Redux Flow with a Real Example5657// 1. User clicks "Add to Cart" button58function ProductCard({ product }) {59 const dispatch = useDispatch();6061 const handleAddToCart = () => {62 dispatch({63 type: 'cart/addItem',64 payload: product65 });66 };6768 return (69 <div>70 <h3>{product.name}</h3>71 <button onClick={handleAddToCart}>Add to Cart</button>72 </div>73 );74}7576// 2. Reducer updates state77function 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}8889// 3. Components re-render90function CartIcon() {91 const itemCount = useSelector(state => state.cart.items.length);9293 return (94 <div>95 🛒 ({itemCount})96 </div>97 );98}99100// Redux DevTools State Example101/*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: 2998112 }113}114115Action Log:116- cart/addItem { payload: { id: 1, name: "iPhone" } }117- user/login { payload: { id: 1, name: "John" } }118- counter/increment119*/
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:
- Less Boilerplate: No need to write action types and creators separately
- Immer Integration: Write "mutating" logic that's actually immutable
- Type Safety: Automatic TypeScript types
- 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 EXAMPLE23// counterSlice.js4import { createSlice, PayloadAction } from '@reduxjs/toolkit';56// Define the shape of your state7interface CounterState {8 value: number;9 step: number;10 history: number[];11}1213// Set initial state14const initialState: CounterState = {15 value: 0,16 step: 1,17 history: [],18};1920// Create the slice21export const counterSlice = createSlice({22 name: 'counter', // Slice name23 initialState, // Initial state24 reducers: { // Reducer functions25 // Action creators are generated automatically!26 increment: (state) => {27 // Thanks to Immer, we can "mutate" state directly28 state.value += state.step;29 state.history.push(state.value);30 },3132 decrement: (state) => {33 state.value -= state.step;34 state.history.push(state.value);35 },3637 incrementByAmount: (state, action: PayloadAction<number>) => {38 state.value += action.payload;39 state.history.push(state.value);40 },4142 setStep: (state, action: PayloadAction<number>) => {43 state.step = action.payload;44 },4546 reset: (state) => {47 // Can also return a new state object48 return initialState;49 },5051 undo: (state) => {52 if (state.history.length > 1) {53 state.history.pop(); // Remove current value54 state.value = state.history[state.history.length - 1] || 0;55 }56 },57 },58});5960// Export actions (auto-generated!)61export const {62 increment,63 decrement,64 incrementByAmount,65 setStep,66 reset,67 undo68} = counterSlice.actions;6970// Export reducer71export default counterSlice.reducer;7273// 🎮 USING THE COUNTER SLICE IN A COMPONENT7475import 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 undo85} from './counterSlice';8687function Counter() {88 // Select state from store89 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);9293 // Get dispatch function94 const dispatch = useDispatch();9596 // Local state for custom amount input97 const [customAmount, setCustomAmount] = React.useState('10');9899 return (100 <div style={{ padding: '20px', maxWidth: '400px', margin: '0 auto' }}>101 <h2>Redux Counter Example</h2>102103 {/* Display current count */}104 <div style={{105 fontSize: '48px',106 fontWeight: 'bold',107 textAlign: 'center',108 margin: '20px 0'109 }}>110 {count}111 </div>112113 {/* Step size control */}114 <div style={{ marginBottom: '20px' }}>115 <label>116 Step Size:117 <input118 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>126127 {/* 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>141142 {/* Custom increment */}143 <div style={{ marginBottom: '20px', textAlign: 'center' }}>144 <input145 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 Amount152 </button>153 </div>154155 {/* Utility buttons */}156 <div style={{157 display: 'flex',158 gap: '10px',159 justifyContent: 'center'160 }}>161 <button onClick={() => dispatch(reset())}>162 Reset163 </button>164 <button165 onClick={() => dispatch(undo())}166 disabled={history.length <= 1}167 >168 Undo169 </button>170 </div>171172 {/* 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 <span178 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}194195// 🛍️ MORE COMPLEX SLICE EXAMPLE - SHOPPING CART196197interface CartItem {198 id: number;199 name: string;200 price: number;201 quantity: number;202}203204interface CartState {205 items: CartItem[];206 total: number;207 discount: number;208}209210const 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);220221 if (existingItem) {222 existingItem.quantity += 1;223 } else {224 state.items.push({ ...action.payload, quantity: 1 });225 }226227 // Recalculate total228 state.total = state.items.reduce(229 (sum, item) => sum + (item.price * item.quantity),230 0231 );232 },233234 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 0239 );240 },241242 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 0253 );254 },255256 applyDiscount: (state, action: PayloadAction<number>) => {257 state.discount = action.payload;258 },259260 clearCart: (state) => {261 return { items: [], total: 0, discount: 0 };262 },263 },264});265266// 💡 SLICE BEST PRACTICES267268// 1. Keep slices focused on one feature269// ❌ Bad: One giant slice for everything270const 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});283284// ✅ Good: Separate slices for each feature285const userSlice = createSlice({ name: 'user', /* ... */ });286const postsSlice = createSlice({ name: 'posts', /* ... */ });287const commentsSlice = createSlice({ name: 'comments', /* ... */ });288289// 2. Name actions clearly290// ❌ Bad: Vague action names291const vagueSice = createSlice({292 name: 'data',293 reducers: {294 update: (state, action) => { /* What are we updating? */ },295 set: (state, action) => { /* Set what? */ },296 }297});298299// ✅ Good: Descriptive action names300const 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:
- Pending: Request in progress
- Fulfilled: Request succeeded
- Rejected: Request failed
createAsyncThunk handles all these states automatically, saving you from writing boilerplate code.
How It Works:
- You define an async function
- Redux Toolkit creates action types for each state
- Your reducer handles these actions
- 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 createAsyncThunk23import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';45// Define types6interface Post {7 id: number;8 title: string;9 body: string;10 userId: number;11}1213interface PostState {14 posts: Post[];15 currentPost: Post | null;16 loading: boolean;17 error: string | null;18}1920// Initial state21const initialState: PostState = {22 posts: [],23 currentPost: null,24 loading: false,25 error: null,26};2728// 🎯 CREATE ASYNC THUNK - Fetch posts from API29export 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);3738// Fetch single post39export 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);4748// 🍰 CREATE THE SLICE49const postSlice = createSlice({50 name: 'posts',51 initialState,52 reducers: {53 // Regular actions54 clearCurrentPost: (state) => {55 state.currentPost = null;56 },57 },58 // Handle async actions59 extraReducers: (builder) => {60 builder61 // Fetch posts cases62 .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 post75 .addCase(fetchPostById.fulfilled, (state, action) => {76 state.currentPost = action.payload;77 });78 },79});8081export const { clearCurrentPost } = postSlice.actions;82export default postSlice.reducer;8384// 🎮 USING IN A COMPONENT85function PostList() {86 const dispatch = useDispatch();87 const { posts, loading, error } = useSelector(state => state.posts);8889 useEffect(() => {90 dispatch(fetchPosts());91 }, [dispatch]);9293 if (loading) return <div>Loading posts...</div>;94 if (error) return <div>Error: {error}</div>;9596 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}108109// 💡 REAL-WORLD EXAMPLE: Authentication110interface AuthState {111 user: { id: string; name: string; email: string } | null;112 token: string | null;113 loading: boolean;114 error: string | null;115}116117// Login async thunk118export 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 });126127 if (!response.ok) {128 const error = await response.json();129 throw new Error(error.message || 'Login failed');130 }131132 return response.json(); // { user, token }133 }134);135136// Auth slice137const 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 builder154 .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});170171// Login component172function LoginForm() {173 const dispatch = useDispatch();174 const { loading, error } = useSelector(state => state.auth);175 const [email, setEmail] = useState('');176 const [password, setPassword] = useState('');177178 const handleSubmit = async (e) => {179 e.preventDefault();180 const result = await dispatch(login({ email, password }));181182 if (login.fulfilled.match(result)) {183 // Navigate to dashboard184 console.log('Login successful!');185 }186 };187188 return (189 <form onSubmit={handleSubmit}>190 {error && <div className="error">{error}</div>}191192 <input193 type="email"194 value={email}195 onChange={(e) => setEmail(e.target.value)}196 placeholder="Email"197 required198 />199200 <input201 type="password"202 value={password}203 onChange={(e) => setPassword(e.target.value)}204 placeholder="Password"205 required206 />207208 <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:
- Action History: See every action dispatched
- State Diff: Compare state before/after actions
- Time Travel: Jump to any previous state
- Import/Export: Save and load state
- 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:
- Normalize State Shape: Keep state flat and avoid nested data
- Use Selectors: Compute derived data with memoization
- Keep Logic in Reducers: Business logic belongs in reducers, not components
- Use TypeScript: Get type safety and better IDE support
- Split Code: Organize by feature, not by file type
1// 🛠️ ENHANCED STORE CONFIGURATION23import { 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';89// Custom middleware for crash reporting10const 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());1718 // Send to crash reporting service19 if (window.Sentry) {20 window.Sentry.captureException(err, {21 extra: {22 action,23 state: store.getState()24 }25 });26 }2728 throw err;29 }30};3132// Custom middleware for analytics33const analytics = (store) => (next) => (action) => {34 // Track specific actions35 if (action.type === 'cart/checkout') {36 window.gtag?.('event', 'purchase', {37 value: store.getState().cart.total,38 items: store.getState().cart.items.length39 });40 }4142 return next(action);43};4445// 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});5253// Configure store with middleware54export const store = configureStore({55 reducer: {56 counter: counterReducer,57 user: userReducer,58 [api.reducerPath]: api.reducer, // RTK Query59 },60 middleware: (getDefaultMiddleware) =>61 getDefaultMiddleware({62 serializableCheck: {63 // Ignore these action types64 ignoredActions: ['persist/PERSIST'],65 // Ignore these paths in state66 ignoredPaths: ['user.lastSeen'],67 },68 thunk: {69 extraArgument: {70 api: 'https://api.example.com',71 },72 },73 })74 .concat(api.middleware) // RTK Query75 .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});8485// 🎯 TYPED HOOKS8687import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';8889export type RootState = ReturnType<typeof store.getState>;90export type AppDispatch = typeof store.dispatch;9192// Use these instead of plain useDispatch and useSelector93export const useAppDispatch = () => useDispatch<AppDispatch>();94export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;9596// 📊 SELECTORS WITH RESELECT9798import { createSelector } from '@reduxjs/toolkit';99100// 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;105106// Memoized selectors (only recalculate when inputs change)107export const selectCurrentUser = createSelector(108 [selectUsers, selectCurrentUserId],109 (users, userId) => users.find(user => user.id === userId)110);111112export const selectCartTotal = createSelector(113 [selectCartItems, selectCartDiscount],114 (items, discount) => {115 const subtotal = items.reduce(116 (sum, item) => sum + (item.price * item.quantity),117 0118 );119 return subtotal * (1 - discount);120 }121);122123export const selectCartItemCount = createSelector(124 [selectCartItems],125 (items) => items.reduce((count, item) => count + item.quantity, 0)126);127128export const selectExpensiveItems = createSelector(129 [selectCartItems],130 (items) => items.filter(item => item.price > 100)131);132133// Parameterized selector134export const makeSelectUserById = () =>135 createSelector(136 [selectUsers, (state: RootState, userId: number) => userId],137 (users, userId) => users.find(user => user.id === userId)138 );139140// Using selectors in components141function CartSummary() {142 const total = useAppSelector(selectCartTotal);143 const itemCount = useAppSelector(selectCartItemCount);144 const expensiveItems = useAppSelector(selectExpensiveItems);145146 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}155156// 🏗️ STATE NORMALIZATION157158// ❌ Bad: Deeply nested state159const 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};179180// ✅ Good: Normalized state181const 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};202203// Entity adapter for normalization204import { createEntityAdapter } from '@reduxjs/toolkit';205206const usersAdapter = createEntityAdapter<User>({207 selectId: (user) => user.id,208 sortComparer: (a, b) => a.name.localeCompare(b.name),209});210211const 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});224225// 📁 FEATURE-BASED FOLDER STRUCTURE226227/*228src/229 features/230 counter/231 counterSlice.ts232 Counter.tsx233 Counter.test.tsx234 user/235 userSlice.ts236 userAPI.ts237 UserList.tsx238 UserProfile.tsx239 cart/240 cartSlice.ts241 Cart.tsx242 CartItem.tsx243 app/244 store.ts245 hooks.ts246 common/247 components/248 utils/249*/250251// 💡 REDUX PATTERNS AND TIPS252253// 1. Action creator patterns254const userSlice = createSlice({255 name: 'user',256 initialState,257 reducers: {258 // Simple action259 logout: (state) => {260 state.currentUser = null;261 },262263 // Action with prepare callback264 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});275276// 2. Handling loading states generically277const 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 builder288 .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};