
Build 10x products in minutes by chatting with AI - beyond just a prototype.
What is middleware in RTK Query?
Is RTK Query better than React Query?
What is the best middleware for Redux?
Managing API requests can get complicated in modern web development. React apps often need to juggle multiple API calls, show loading states, process what comes back, and handle errors. That's where React Query middleware comes in handy. It gives you the tools to simplify all that.
In this blog, we'll walk you through building custom middleware with RTK Query, part of the Redux toolkit, to make API requests easier to handle.
RTK Query from redux toolkit offers a structured approach to API management. The middleware system in RTK Query acts as a pipeline that processes API requests before they reach the server and after the responses are returned.
Middleware functions in RTK Query intercept and process API requests and responses. They sit between your application and the API endpoints, allowing you to:
• Modify requests before they're sent
• Transform responses before they reach your components
• Handle errors consistently
• Log API activity
• Add authentication tokens
Let's visualize the middleware flow:
Before we dive into custom middleware, let's set up RTK Query in a React app.
First, install the necessary packages:
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react' import { configureStore } from '@reduxjs/toolkit' import { setupListeners } from '@reduxjs/toolkit/query' // Create an API service const api = createApi({ reducerPath: 'api', baseQuery: fetchBaseQuery({ baseUrl: 'https://api.example.com' }), endpoints: (builder) => ({ getUsers: builder.query({ query: () => 'users', }), getUserById: builder.query({ query: (id) => `users/${id}`, }), }), }) // Export hooks for usage in components export const { useGetUsersQuery, useGetUserByQueryQuery } = api // Configure the store const store = configureStore({ reducer: { [api.reducerPath]: api.reducer, }, middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(api.middleware), }) // Enable refetchOnFocus and refetchOnReconnect setupListeners(store.dispatch)
In this setup, we've created an API slice with two endpoints and configured our redux store to use the default middleware plus the RTK Query middleware.
Now, let's explore how to create custom middleware for RTK Query to handle specific requirements.
An everyday use case for middleware is adding authentication tokens to requests:
import { fetchBaseQuery } from '@reduxjs/toolkit/query/react' // Create a custom base query with auth headers const baseQueryWithAuth = fetchBaseQuery({ baseUrl: 'https://api.example.com', prepareHeaders: (headers, { getState }) => { // Get token from state const token = getState().auth.token // Add token to headers if it exists if (token) { headers.set('authorization', `Bearer ${token}`) } return headers }, }) // Use in API definition const api = createApi({ reducerPath: 'api', baseQuery: baseQueryWithAuth, endpoints: (builder) => ({ // endpoints here }), })
This middleware automatically adds the authentication token to every request.
Another powerful application is centralized error handling:
import { fetchBaseQuery } from '@reduxjs/toolkit/query/react' // Create error handling middleware const baseQueryWithErrorHandling = async (args, api, extraOptions) => { const baseQuery = fetchBaseQuery({ baseUrl: 'https://api.example.com' }) try { // Attempt the request const result = await baseQuery(args, api, extraOptions) // Check for error status codes if (result.error) { // Handle specific error codes if (result.error.status === 401) { // Dispatch logout action or refresh token api.dispatch({ type: 'auth/logout' }) } // Log all errors console.error('API error:', result.error) } return result } catch (error) { // Handle unexpected errors return { error: { status: 'FETCH_ERROR', error: String(error) } } } } // Use in API definition const api = createApi({ reducerPath: 'api', baseQuery: baseQueryWithErrorHandling, endpoints: (builder) => ({ // endpoints here }), })
This middleware catches and processes errors from API calls, providing a central place to handle authentication failures or server errors.
Let's explore more advanced middleware applications with RTK Query.
RTK Query includes built-in caching, but you can enhance it with custom middleware:
This setup automatically invalidates the cache for user data when any user is updated, triggering a data refresh.
You can transform requests and responses with middleware:
This middleware transforms outgoing and incoming requests, standardizing date formats and adding computed values.
For complex applications, you might need to combine multiple middleware functions:
This example chains three middleware functions together:
withAuth adds authentication
withLogging logs requests and responses
withErrorHandling handles errors
Let's see how middleware can solve real-world problems:
RTK Query automatically tracks loading states, making it easy to show loading indicators.
This middleware applies optimistic updates, immediately updating the UI before the server confirms the change.
When implementing middleware, consider these performance tips:
RTK Query middleware provides a powerful system for handling API requests in React applications. You can centralize authentication, error handling, logging, and data transformation by implementing custom middleware, making your code more maintainable and robust.
The middleware approach in RTK Query offers a structured way to manage the complexity of API interactions, letting you focus on building features rather than managing request logic. Applying the techniques in this guide allows you to create more efficient and reliable React applications.
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
// Create API with custom cache invalidation
const api = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({ baseUrl: 'https://api.example.com' }),
tagTypes: ['User', 'Post'],
endpoints: (builder) => ({
getUsers: builder.query({
query: () => 'users',
providesTags: ['User'],
}),
updateUser: builder.mutation({
query: (user) => ({
url: `users/${user.id}`,
method: 'PATCH',
body: user,
}),
invalidatesTags: ['User'],
}),
getPosts: builder.query({
query: () => 'posts',
providesTags: ['Post'],
}),
}),
})import { fetchBaseQuery } from '@reduxjs/toolkit/query/react'
// Create transformation middleware
const transformationMiddleware = async (args, api, extraOptions) => {
const baseQuery = fetchBaseQuery({ baseUrl: 'https://api.example.com' })
// Transform request before sending
let transformedArgs = args
if (args.body) {
// Convert dates to ISO strings for API
transformedArgs = {
...args,
body: JSON.parse(JSON.stringify(args.body).replace(
/"\d{4}-d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z"/g,
match => `"${new Date(JSON.parse(match)).toISOString()}"`
))
}
}
// Make the request
const result = await baseQuery(transformedArgs, api, extraOptions)
// Transform response before returning
if (result.data) {
// Add computed values or transform data
return {
...result,
data: Array.isArray(result.data)
? result.data.map(item => ({ ...item, fullName: `${item.firstName} ${item.lastName}` }))
: { ...result.data, fullName: `${result.data.firstName} ${result.data.lastName}` }
}
}
return result
}
// Use in API definition
const api = createApi({
reducerPath: 'api',
baseQuery: transformationMiddleware,
endpoints: (builder) => ({
// endpoints here
}),
})import { fetchBaseQuery } from '@reduxjs/toolkit/query/react'
// Authentication middleware
const withAuth = async (args, api, extraOptions) => {
const baseQuery = fetchBaseQuery({
baseUrl: 'https://api.example.com',
prepareHeaders: (headers, { getState }) => {
const token = getState().auth.token
if (token) {
headers.set('authorization', `Bearer ${token}`)
}
return headers
},
})
return baseQuery(args, api, extraOptions)
}
// Logging middleware that wraps authentication middleware
const withLogging = async (args, api, extraOptions) => {
console.log('Request:', args)
const startTime = Date.now()
const result = await withAuth(args, api, extraOptions)
const duration = Date.now() - startTime
if (result.error) {
console.error('Error:', result.error, `(${duration}ms)`)
} else {
console.log('Response:', result.data, `(${duration}ms)`)
}
return result
}
// Error handling middleware that wraps logging middleware
const withErrorHandling = async (args, api, extraOptions) => {
try {
const result = await withLogging(args, api, extraOptions)
if (result.error) {
if (result.error.status === 401) {
api.dispatch({ type: 'auth/logout' })
}
}
return result
} catch (error) {
return { error: { status: 'FETCH_ERROR', error: String(error) } }
}
}
// Use combined middleware in API definition
const api = createApi({
reducerPath: 'api',
baseQuery: withErrorHandling,
endpoints: (builder) => ({
// endpoints here
}),
})import React from 'react'
import { useGetUsersQuery } from './api'
function UserList() {
const { data: users, isLoading, error } = useGetUsersQuery()
if (isLoading) {
return <div>Loading users...</div>
}
if (error) {
return <div>Error loading users: {error.message}</div>
}
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
)
}const api = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({ baseUrl: 'https://api.example.com' }),
tagTypes: ['Post'],
endpoints: (builder) => ({
getPosts: builder.query({
query: () => 'posts',
providesTags: ['Post'],
}),
updatePost: builder.mutation({
query: (post) => ({
url: `posts/${post.id}`,
method: 'PATCH',
body: post,
}),
async onQueryStarted({ id, ...patch }, { dispatch, queryFulfilled }) {
// Optimistically update the post in the cache
const patchResult = dispatch(
api.util.updateQueryData('getPosts', undefined, draft => {
const post = draft.find(post => post.id === id)
if (post) {
Object.assign(post, patch)
}
})
)
try {
// Wait for the request to complete
await queryFulfilled
} catch {
// If it fails, revert the optimistic update
patchResult.undo()
}
},
invalidatesTags: ['Post'],
}),
}),
})