Custom Middleware
Middleware runs between dispatching an action and it reaching the reducer. It's useful for logging, API calls, and other side effects.
Basic Middleware
const loggerMiddleware = (store) => (next) => (action) => {
console.log('Action:', action.type)
console.log('Before:', store.getState())
const result = next(action)
console.log('After:', store.getState())
return result
}
const store = configureStore({
reducer: rootReducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(loggerMiddleware),
})
How Middleware Works
// This is the pattern:
const middleware = (store) => (next) => (action) => {
// Do something before action reaches reducer
const result = next(action) // Pass action to next middleware or reducer
// Do something after reducer runs
return result
}
Common Examples
API Middleware
const apiMiddleware = (store) => (next) => (action) => {
// Check if action has API call info
if (action.type === 'api/request') {
fetch(action.payload.url)
.then(response => response.json())
.then(data => {
store.dispatch({
type: action.payload.successType,
payload: data,
})
})
.catch(error => {
store.dispatch({
type: action.payload.errorType,
payload: error.message,
})
})
}
return next(action)
}
Analytics Middleware
const analyticsMiddleware = (store) => (next) => (action) => {
// Track certain actions
if (action.type.includes('user/')) {
analytics.track('User Action', {
action: action.type,
timestamp: new Date().toISOString(),
})
}
return next(action)
}
Local Storage Middleware
const localStorageMiddleware = (store) => (next) => (action) => {
const result = next(action)
// Save certain state to local storage
if (action.type.includes('auth/')) {
const state = store.getState()
localStorage.setItem('authToken', state.auth.token)
}
return result
}
Adding Multiple Middleware
const store = configureStore({
reducer: rootReducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware()
.concat(loggerMiddleware)
.concat(analyticsMiddleware)
.concat(localStorageMiddleware),
})
Conditional Middleware
const store = configureStore({
reducer: rootReducer,
middleware: (getDefaultMiddleware) => {
const middleware = getDefaultMiddleware()
// Only add logger in development
if (process.env.NODE_ENV === 'development') {
middleware.concat(loggerMiddleware)
}
return middleware
},
})
Error Handling Middleware
const errorMiddleware = (store) => (next) => (action) => {
try {
return next(action)
} catch (error) {
console.error('Redux Error:', error)
// Dispatch error action
store.dispatch({
type: 'error/actionFailed',
payload: {
action: action.type,
error: error.message,
},
})
// Don't break the app
return { type: 'error/handled' }
}
}
Async Middleware (Custom Thunk)
const customThunkMiddleware = (store) => (next) => (action) => {
// If action is a function, call it with dispatch and getState
if (typeof action === 'function') {
return action(store.dispatch, store.getState)
}
// Otherwise, pass it along
return next(action)
}
// Usage
const fetchUser = (userId) => async (dispatch, getState) => {
dispatch({ type: 'user/loading' })
try {
const response = await fetch(`/api/users/${userId}`)
const user = await response.json()
dispatch({ type: 'user/loaded', payload: user })
} catch (error) {
dispatch({ type: 'user/error', payload: error.message })
}
}
Debug Middleware
const debugMiddleware = (store) => (next) => (action) => {
const start = Date.now()
const result = next(action)
const end = Date.now()
console.log(`Action ${action.type} took ${end - start}ms`)
return result
}
Batching Middleware
const batchMiddleware = (store) => (next) => {
let batch = []
let timeoutId = null
return (action) => {
if (action.type === 'batch/start') {
batch = []
return
}
if (action.type === 'batch/end') {
batch.forEach(batchedAction => next(batchedAction))
batch = []
return
}
// Add to batch if batching is active
if (batch.length >= 0 && action.meta?.batch) {
batch.push(action)
// Auto-flush batch after delay
if (timeoutId) clearTimeout(timeoutId)
timeoutId = setTimeout(() => {
batch.forEach(batchedAction => next(batchedAction))
batch = []
}, 10)
return
}
return next(action)
}
}
Testing Middleware
// Test setup
const mockStore = {
getState: jest.fn(() => ({})),
dispatch: jest.fn(),
}
const next = jest.fn()
test('logger middleware logs actions', () => {
const action = { type: 'test/action' }
loggerMiddleware(mockStore)(next)(action)
expect(next).toHaveBeenCalledWith(action)
})
Key Points
- Order matters - Middleware runs in the order you add it
- Call next() - Always call next(action) to continue the chain
- Return result - Return the result from next() or your own value
- Side effects - Perfect place for logging, analytics, API calls
- Don't mutate - Don't change the action object