Redux Middleware Mastery 2026: Async Patterns + Production Scale 🚀
Redux middleware intercepts actions before they reach reducers, enabling async logic, API calls, logging, authentication, error handling, and analytics in a predictable, testable way. Redux Toolkit provides createAsyncThunk for complex async flows and RTK Query for 90% of API use cases with automatic caching, optimistic updates, and TypeScript-first patterns.
🎯 Middleware Flow (Action Pipeline)
Component → Action → [MIDDLEWARE 1] → [MIDDLEWARE 2] → Reducer → State ↑ Auth ↑ Logging ↑ Async Thunk
🏗️ Production Middleware Stack
1. Redux Thunk (createAsyncThunk) - Complex Async
// Complex async with loading/error states
export const fetchUsers = createAsyncThunk<
User[],
void,
{ rejectValue: string }
>(
'users/fetchUsers',
async (_, { rejectWithValue }) => {
try {
const token = localStorage.getItem('token');
const response = await fetch('/api/users', {
headers: { Authorization: `Bearer ${token}` }
});
if (!response.ok) {
return rejectWithValue('Failed to fetch users');
}
return response.json();
} catch (error) {
return rejectWithValue(error.message);
}
}
);
Slice Integration:
const usersSlice = createSlice({
name: 'users',
initialState: {
data: [] as User[],
loading: false,
error: null as string | null
},
extraReducers: (builder) => {
builder
.addCase(fetchUsers.pending, (state) => {
state.loading = true;
state.error = null;
})
.addCase(fetchUsers.fulfilled, (state, action) => {
state.loading = false;
state.data = action.payload;
})
.addCase(fetchUsers.rejected, (state, action) => {
state.loading = false;
state.error = action.payload ?? 'Unknown error';
});
}
});
2. RTK Query (90% API Use Cases)
// services/api.ts - Automatic caching + mutations
export const api = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({
baseUrl: '/api/',
prepareHeaders: (headers, { getState }) => {
const token = (getState() as RootState).auth.token;
if (token) headers.set('authorization', `Bearer ${token}`);
return headers;
}
}),
tagTypes: ['User', 'Post', 'Comment'],
endpoints: (builder) => ({
getUsers: builder.query<User[], void>({
query: () => 'users',
providesTags: ['User']
}),
createPost: builder.mutation<Post, Partial<Post>>({
query: (newPost) => ({
url: 'posts',
method: 'POST',
body: newPost
}),
invalidatesTags: ['Post'] // Auto-refetch lists
})
})
});
export const { useGetUsersQuery, useCreatePostMutation } = api;
🛠️ Custom Middleware Patterns
1. Auth Interceptor Middleware
// middleware/authMiddleware.ts
import { Middleware } from '@reduxjs/toolkit';
export const authMiddleware: Middleware = (store) => (next) => (action) => {
// Add auth token to all API calls
if (api.util.resetApiState.match(action)) return next(action);
const state = store.getState();
const token = (state as RootState).auth.token;
if (token && typeof action === 'object' && 'meta' in action) {
action.meta.headers = {
...action.meta.headers,
Authorization: `Bearer ${token}`
};
}
return next(action);
};
2. Logging Middleware (Production Ready)
// middleware/logger.ts
export const loggerMiddleware: Middleware =
(store) => (next) => (action) => {
if (process.env.NODE_ENV === 'development') {
console.group(`%cAction: ${action.type}`, 'color: #2196F3');
console.log('%cPrevious State:', 'color: #9E9E9E', store.getState());
console.log('%cAction:', 'color: #03A9F4', action);
const result = next(action);
console.log('%cNext State:', 'color: #4CAF50', store.getState());
console.groupEnd();
return result;
}
return next(action);
};
3. Error Boundary Middleware
// middleware/errorHandler.ts
export const errorHandlerMiddleware: Middleware =
(store) => (next) => (action) => {
try {
return next(action);
} catch (err) {
console.error('Redux action error:', err);
// Dispatch error action
store.dispatch({
type: 'GLOBAL/setError',
payload: { message: err.message, timestamp: Date.now() }
});
// Re-throw for React error boundaries
throw err;
}
};
🎯 Store Configuration (Production)
// store/index.ts
import { configureStore } from '@reduxjs/toolkit';
import { api } from './services/api';
export const store = configureStore({
reducer: {
[api.reducerPath]: api.reducer,
auth: authSlice,
ui: uiSlice
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: {
ignoredActions: ['persist/PERSIST']
}
})
.concat(api.middleware)
.concat(loggerMiddleware, authMiddleware),
devTools: process.env.NODE_ENV !== 'production'
});
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
🚀 Component Usage Patterns
RTK Query (Recommended)
function TodoList() {
const { data: todos = [], isLoading, error } = useGetTodosQuery();
const [deleteTodo] = useDeleteTodoMutation();
if (isLoading) return <Spinner />;
if (error) return <ErrorMessage error={error} />;
return (
<ul>
{todos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
onDelete={() => deleteTodo(todo.id)}
/>
))}
</ul>
);
}
Async Thunk (Complex Logic)
function UserProfile() {
const dispatch = useAppDispatch();
const { data: user } = useGetUserQuery(userId);
const handleUpgradePlan = () => {
dispatch(upgradePlanAsync({ plan: 'pro', userId }));
};
return (
<div>
<h1>{user?.name}</h1>
<button onClick={handleUpgradePlan}>
Upgrade to Pro
</button>
</div>
);
}
📊 RTK Query vs Thunk Decision Matrix
| Use Case | RTK Query | Async Thunk |
|---|---|---|
| Simple CRUD | ✅ Automatic | ❌ Manual |
| Caching | ✅ Built-in | ❌ Custom |
| Optimistic | ✅ Native | ❌ Manual |
| Complex Logic | ❌ Limited | ✅ Full control |
| Team Scale | ✅ Convention | ❌ Custom |
🎯 Production Checklist
✅ [] RTK Query for 90% API calls ✅ [] createAsyncThunk for complex flows ✅ [] Auth interceptor middleware ✅ [] Logging middleware (dev only) ✅ [] Error boundaries + global error action ✅ [] TypeScript error types (rejectWithValue) ✅ [] DevTools time-travel enabled ✅ [] Store middleware order: api → auth → logger
🔥 Advanced Patterns
Optimistic Updates (RTK Query)
createTodo: builder.mutation({
query: (todo) => ({
url: 'todos',
method: 'POST',
body: todo
}),
async onQueryStarted({ draftId }, { dispatch, queryFulfilled }) {
// Optimistic update
const patchResult = dispatch(
api.util.updateQueryData('getTodos', undefined, (draft) => {
draft.push({ id: draftId, ...todo, pending: true });
})
);
try {
await queryFulfilled;
} catch {
patchResult.undo();
}
}
})
Infinite Scroll (RTK Query)
getTodos: builder.query({
query: (page) => `todos?page=${page}&limit=20`,
serializeQueryArgs: ({ endpointName, queryArgs }) => {
return endpointName;
},
merge: (currentCache, newItems) => {
return [...currentCache, ...newItems];
},
forceRefetch({ currentArg, previousArg }) {
return currentArg !== previousArg;
}
})
🎯 Final Thoughts
Redux middleware = Production async superpower. RTK Query handles 90% of API cases with zero boilerplate, while createAsyncThunk gives full control for complex business logic. Custom middleware adds auth, logging, and error handling that scale to enterprise teams.
2026 Async Strategy:
RTK Query → 90% CRUD + caching
Async Thunk → Complex workflows
Custom Middleware → Auth/logging
Server Components → Initial data
Master middleware → Enterprise scale. Build predictable async flows with automatic caching and TypeScript safety 🚀.