Redux Core Advanced 15 min read

Redux Middleware Mastery 2026: Async Patterns + RTK Query 🚀

Complete guide to Redux middleware including Redux Thunk, RTK Query, custom middleware, logging, authentication interceptors, error boundaries, and production patterns that scale to enterprise apps.

#redux middleware #thunk #async redux #rtk query #custom middleware
Guide Redux Core

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

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 CaseRTK QueryAsync Thunk
Simple CRUDAutomatic❌ Manual
CachingBuilt-in❌ Custom
OptimisticNative❌ Manual
Complex Logic❌ LimitedFull control
Team ScaleConvention❌ 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 🚀.

Chat with us