Documentation Index Fetch the complete documentation index at: https://mintlify.com/academind/react-complete-guide-course-resources/llms.txt
Use this file to discover all available pages before exploring further.
Overview
Learn advanced Redux patterns for handling asynchronous operations, side effects, and complex state updates. Master Redux Thunks for API calls, optimistic updates, and advanced data flow patterns.
Redux Thunks Handle async logic in Redux
Side Effects Manage API calls and data fetching
Action Creators Create complex action logic
Error Handling Handle loading and error states
The Challenge: Async Logic
Reducers must be pure, synchronous functions. But real apps need to:
Fetch data from APIs
Save data to backend
Wait for user input
Perform calculations with delays
Handle WebSocket messages
Two main approaches:
Inside Components - useEffect with fetch
Inside Action Creators - Redux Thunks
Thunks keep components clean and logic testable.
Reducers Must Stay Pure: Never do this in a reducer:
Fetch data
Call setTimeout/setInterval
Generate random values
Modify variables outside reducer
These operations must happen elsewhere.
Redux Thunk Basics
A thunk is a function that returns another function, allowing delayed or async logic.
Regular Action Creator
Thunk Action Creator
// Returns action object immediately
export const increment = () => {
return { type: 'counter/increment' };
};
// Use it
dispatch ( increment ());
// Returns function that receives dispatch
export const incrementAsync = () => {
return ( dispatch ) => {
setTimeout (() => {
dispatch ({ type: 'counter/increment' });
}, 1000 );
};
};
// Use it (Redux handles the function)
dispatch ( incrementAsync ());
How Thunks Work:
You dispatch a thunk (function)
Redux middleware intercepts it
Middleware calls the function with dispatch and getState
Function performs async work
Function dispatches regular actions when ready
Here’s a real implementation from the course showing async data fetching and saving.
Cart Slice
import { createSlice } from '@reduxjs/toolkit' ;
const cartSlice = createSlice ({
name: 'cart' ,
initialState: {
items: [],
totalQuantity: 0 ,
changed: false ,
},
reducers: {
replaceCart ( state , action ) {
state . totalQuantity = action . payload . totalQuantity ;
state . items = action . payload . items ;
},
addItemToCart ( state , action ) {
const newItem = action . payload ;
const existingItem = state . items . find (( item ) => item . id === newItem . id );
state . totalQuantity ++ ;
state . changed = true ;
if ( ! existingItem ) {
state . items . push ({
id: newItem . id ,
price: newItem . price ,
quantity: 1 ,
totalPrice: newItem . price ,
name: newItem . title ,
});
} else {
existingItem . quantity ++ ;
existingItem . totalPrice = existingItem . totalPrice + newItem . price ;
}
},
removeItemFromCart ( state , action ) {
const id = action . payload ;
const existingItem = state . items . find (( item ) => item . id === id );
state . totalQuantity -- ;
state . changed = true ;
if ( existingItem . quantity === 1 ) {
state . items = state . items . filter (( item ) => item . id !== id );
} else {
existingItem . quantity -- ;
existingItem . totalPrice = existingItem . totalPrice - existingItem . price ;
}
},
},
});
export const cartActions = cartSlice . actions ;
export default cartSlice ;
Async Action Creators (Thunks)
import { uiActions } from './ui-slice' ;
import { cartActions } from './cart-slice' ;
// Thunk to fetch cart data from backend
export const fetchCartData = () => {
return async ( dispatch ) => {
const fetchData = async () => {
const response = await fetch (
'https://react-http-6b4a6.firebaseio.com/cart.json'
);
if ( ! response . ok ) {
throw new Error ( 'Could not fetch cart data!' );
}
const data = await response . json ();
return data ;
};
try {
const cartData = await fetchData ();
dispatch (
cartActions . replaceCart ({
items: cartData . items || [],
totalQuantity: cartData . totalQuantity ,
})
);
} catch ( error ) {
dispatch (
uiActions . showNotification ({
status: 'error' ,
title: 'Error!' ,
message: 'Fetching cart data failed!' ,
})
);
}
};
};
// Thunk to send cart data to backend
export const sendCartData = ( cart ) => {
return async ( dispatch ) => {
dispatch (
uiActions . showNotification ({
status: 'pending' ,
title: 'Sending...' ,
message: 'Sending cart data!' ,
})
);
const sendRequest = async () => {
const response = await fetch (
'https://react-http-6b4a6.firebaseio.com/cart.json' ,
{
method: 'PUT' ,
body: JSON . stringify ({
items: cart . items ,
totalQuantity: cart . totalQuantity ,
}),
}
);
if ( ! response . ok ) {
throw new Error ( 'Sending cart data failed.' );
}
};
try {
await sendRequest ();
dispatch (
uiActions . showNotification ({
status: 'success' ,
title: 'Success!' ,
message: 'Sent cart data successfully!' ,
})
);
} catch ( error ) {
dispatch (
uiActions . showNotification ({
status: 'error' ,
title: 'Error!' ,
message: 'Sending cart data failed!' ,
})
);
}
};
};
Using Thunks in Components
import { useEffect } from 'react' ;
import { useSelector , useDispatch } from 'react-redux' ;
import { fetchCartData , sendCartData } from './store/cart-actions' ;
let isInitial = true ;
function App () {
const dispatch = useDispatch ();
const cart = useSelector (( state ) => state . cart );
const notification = useSelector (( state ) => state . ui . notification );
// Fetch cart data on mount
useEffect (() => {
dispatch ( fetchCartData ());
}, [ dispatch ]);
// Send cart data whenever it changes
useEffect (() => {
if ( isInitial ) {
isInitial = false ;
return ;
}
if ( cart . changed ) {
dispatch ( sendCartData ( cart ));
}
}, [ cart , dispatch ]);
return (
<>
{ notification && (
< Notification
status = { notification . status }
title = { notification . title }
message = { notification . message }
/>
) }
< Layout >
< Cart />
< Products />
</ Layout >
</>
);
}
Thunk Patterns
Basic Thunk
Async Thunk
Thunk with getState
Thunk with Parameters
export const simpleThunk = () => {
return ( dispatch ) => {
// Perform sync or async work
dispatch ( someAction ());
};
};
export const asyncThunk = () => {
return async ( dispatch ) => {
const response = await fetch ( '/api/data' );
const data = await response . json ();
dispatch ( setData ( data ));
};
};
export const conditionalThunk = () => {
return ( dispatch , getState ) => {
const state = getState ();
if ( state . user . isAuthenticated ) {
dispatch ( fetchUserData ());
}
};
};
export const fetchUser = ( userId ) => {
return async ( dispatch ) => {
const response = await fetch ( `/api/users/ ${ userId } ` );
const data = await response . json ();
dispatch ( setUser ( data ));
};
};
// Use it
dispatch ( fetchUser ( 123 ));
Error Handling
Handle errors gracefully in thunks:
With Try-Catch
Helper Function Pattern
export const fetchData = () => {
return async ( dispatch ) => {
dispatch ( setLoading ( true ));
try {
const response = await fetch ( '/api/data' );
if ( ! response . ok ) {
throw new Error ( 'Request failed' );
}
const data = await response . json ();
dispatch ( setData ( data ));
dispatch ( setError ( null ));
} catch ( error ) {
dispatch ( setError ( error . message ));
} finally {
dispatch ( setLoading ( false ));
}
};
};
Error Handling Best Practices:
Always catch errors in thunks
Dispatch error actions to update UI
Include helpful error messages
Log errors for debugging
Show user-friendly messages
Loading States and UI Feedback
Manage loading and notification states:
import { createSlice } from '@reduxjs/toolkit' ;
const uiSlice = createSlice ({
name: 'ui' ,
initialState: {
notification: null ,
},
reducers: {
showNotification ( state , action ) {
state . notification = {
status: action . payload . status , // 'pending' | 'success' | 'error'
title: action . payload . title ,
message: action . payload . message ,
};
},
hideNotification ( state ) {
state . notification = null ;
},
},
});
export const uiActions = uiSlice . actions ;
export default uiSlice ;
Notification Component
const Notification = ({ status , title , message }) => {
let specialClasses = '' ;
if ( status === 'error' ) {
specialClasses = 'error' ;
}
if ( status === 'success' ) {
specialClasses = 'success' ;
}
const cssClasses = `notification ${ specialClasses } ` ;
return (
< section className = { cssClasses } >
< h2 > { title } </ h2 >
< p > { message } </ p >
</ section >
);
};
Optimistic Updates
Update UI immediately, then sync with backend:
export const addItemOptimistic = ( item ) => {
return async ( dispatch ) => {
// 1. Update UI immediately
dispatch ( cartActions . addItemToCart ( item ));
try {
// 2. Send to backend
await fetch ( '/api/cart' , {
method: 'POST' ,
body: JSON . stringify ( item ),
});
} catch ( error ) {
// 3. Revert on error
dispatch ( cartActions . removeItemFromCart ( item . id ));
dispatch ( uiActions . showNotification ({
status: 'error' ,
title: 'Error!' ,
message: 'Could not add item.' ,
}));
}
};
};
When to Use Optimistic Updates:
High probability of success
Fast user feedback needed
Easy to revert on error
When NOT to Use:
Critical operations (payments)
Complex error scenarios
Difficult to revert changes
Redux Thunk vs useEffect
useEffect Approach
Redux Thunk Approach
function Component () {
const [ data , setData ] = useState ( null );
useEffect (() => {
async function fetchData () {
const response = await fetch ( '/api/data' );
const result = await response . json ();
setData ( result );
}
fetchData ();
}, []);
// Component code
}
Pros:
Simple for component-specific data
Easy to understand
Cons:
Logic tied to component
Hard to reuse
Difficult to test
// cart-actions.js
export const fetchData = () => {
return async ( dispatch ) => {
const response = await fetch ( '/api/data' );
const result = await response . json ();
dispatch ( setData ( result ));
};
};
// Component.js
function Component () {
const dispatch = useDispatch ();
useEffect (() => {
dispatch ( fetchData ());
}, [ dispatch ]);
// Component code
}
Pros:
Reusable logic
Easy to test
Centralized state management
Cons:
More boilerplate
Requires understanding thunks
Avoiding Infinite Loops
Prevent sending data on initial load:
let isInitial = true ;
function App () {
const cart = useSelector (( state ) => state . cart );
useEffect (() => {
// Skip first run (initial cart load)
if ( isInitial ) {
isInitial = false ;
return ;
}
// Only send if cart actually changed
if ( cart . changed ) {
dispatch ( sendCartData ( cart ));
}
}, [ cart , dispatch ]);
}
Common Pitfalls:
Sending data on initial load
Fetching triggering another fetch
Incorrect dependency arrays
Missing cleanup functions
Advanced Patterns
export const loginUser = ( credentials ) => {
return async ( dispatch ) => {
// 1. Login
const authResponse = await fetch ( '/api/login' , {
method: 'POST' ,
body: JSON . stringify ( credentials ),
});
const authData = await authResponse . json ();
dispatch ( setAuthToken ( authData . token ));
// 2. Fetch user data
dispatch ( fetchUserData ());
// 3. Fetch user preferences
dispatch ( fetchUserPreferences ());
};
};
export const fetchIfNeeded = () => {
return ( dispatch , getState ) => {
const state = getState ();
// Only fetch if data is stale
if ( ! state . data . items . length || state . data . isStale ) {
dispatch ( fetchData ());
}
};
};
let timeoutId ;
export const searchDebounced = ( query ) => {
return ( dispatch ) => {
// Clear previous timeout
if ( timeoutId ) {
clearTimeout ( timeoutId );
}
// Set new timeout
timeoutId = setTimeout (() => {
dispatch ( fetchSearchResults ( query ));
}, 500 );
};
};
let controller ;
export const fetchWithCancel = () => {
return async ( dispatch ) => {
// Cancel previous request
if ( controller ) {
controller . abort ();
}
// Create new controller
controller = new AbortController ();
try {
const response = await fetch ( '/api/data' , {
signal: controller . signal ,
});
const data = await response . json ();
dispatch ( setData ( data ));
} catch ( error ) {
if ( error . name !== 'AbortError' ) {
dispatch ( setError ( error . message ));
}
}
};
};
Testing Thunks
import configureMockStore from 'redux-mock-store' ;
import thunk from 'redux-thunk' ;
import { fetchData } from './cart-actions' ;
const mockStore = configureMockStore ([ thunk ]);
test ( 'fetchData dispatches correct actions' , async () => {
const store = mockStore ({});
await store . dispatch ( fetchData ());
const actions = store . getActions ();
expect ( actions [ 0 ]). toEqual ({ type: 'data/setLoading' });
expect ( actions [ 1 ]). toEqual ({ type: 'data/setData' , payload: /* ... */ });
});
Best Practices
Keep Thunks Focused Each thunk should handle one async operation
Always Handle Errors Use try-catch and dispatch error actions
Show Loading States Give users feedback during async operations
Avoid Infinite Loops Be careful with useEffect dependencies
Extract API Calls Separate API logic into service functions
Type Your Actions Use TypeScript or constants for action types
Alternative: RTK Query
For complex data fetching, consider RTK Query:
import { createApi , fetchBaseQuery } from '@reduxjs/toolkit/query/react' ;
export const api = createApi ({
baseQuery: fetchBaseQuery ({ baseUrl: '/api' }),
endpoints : ( builder ) => ({
getCart: builder . query ({
query : () => 'cart' ,
}),
updateCart: builder . mutation ({
query : ( cart ) => ({
url: 'cart' ,
method: 'PUT' ,
body: cart ,
}),
}),
}),
});
RTK Query provides:
Automatic caching
Automatic refetching
Loading and error states
Optimistic updates
Request deduplication
Consider it for data-heavy applications.
Resources
Redux Thunk Docs Official Redux Thunk documentation
RTK Query Powerful data fetching solution