Aspect-Oriented Programming helpers for Redux middleware
npm install redux-aop
This is a collection of helper functions (or "combinators") for common patterns in redux middleware. It borrows ideas and terminology from Aspect-Oriented programming, and may remind you of alias_method_chain
or before_action
in Ruby on Rails.
Here's a simple logger:
import { after } from "redux-aop"
const loggerMiddleware = after((store, action) => {
console.log(`${action.type}: ${new Date().toTimeString().split(' ')[0]}`, action)
console.log("next state:", store.getState())
return action
})
redux-thunk:
import { before } from "redux-aop"
const thunkMiddleware = before(
(action) => typeof action === "function",
(store, thunk) => { thunk(store.dispatch, store.getState) })
An action debouncer:
import { before } from "redux-aop"
const debounce = (match, time, immediate = false) => {
let timeoutHook = null
return before(match, (store, action) => {
clearTimeout(timeoutHook)
setTimeout(() => {
timeoutHook = null
if (!immediate) { store.dispatch(action) }
}, time)
if (immediate && !timeout) { store.dispatch(action) }
})
}
As a low-level library, Redux is designed for maximum flexibility at the expense of convenience. This is perhaps most visible in middleware -- the store => next => action
function signature is great for complex middleware but overkill for many cases. The relative complexity of the middleware API discourages people from using it directly, instead relying on third-party libraries, even for simple tasks like logging and error reporting.
This library is significantly smaller in size and scope than effect-management libraries like redux-loop or redux-saga, but can improve the ergonomics of using the base middleware API directly such that those libraries might not be needed.
Absolutely.
import { match, before, after, around, not } from "redux-aop"
Limit the actions handled by a middleware to those matched by a matcher. You will probably not use this function directly, but its matching is used in before
, after
and around
.
- matcher: [String|Array|(action) => Boolean]
- if matcher is a string, matches when
action.type === matcher
. - if matcher is an array of strings, matches when the array includes
action.type
. - if matcher is a function, matches when
matcher(action)
returns truthy.
- if matcher is a string, matches when
- middleware: A plain redux middleware.
Create a middleware that handles actions before the reducer. A common use for this is intercepting a dispatched value, like a thunk or a promise, that the reducer cannot handle by itself. This can also be used for adding additional data to actions, like timestamps or unique IDs.
- matcher (Optional)
(store, action) => action
: If this function returns a value, that value continues through the middleware and into the reducer. If the function returnsundefined
, the dispatch cycle ends and the subsequent middleware & reducer will not receive any value.
Create a middleware that handles actions after the reducer. This is useful for middleware (like loggers) that need the store's state after the reducer has updated it.
Caveats: after
middleware will run in the opposite order they are listed in applyMiddleware
. For example, given:
createStore(reducer, applyMiddleware(before(m1), before(m2), after(m3), after(m4)))
These will run in the order m1, m2, reducer, m4, m3
.
- matcher (Optional)
(store, action) => action
Create a middleware that handles actions around the reducer. This is less useful than before
and after
as this is already the normal behavior of middleware, but at least this adds matching and uncurries the function signature.
- matcher (Optional)
(store, next, action) => action
Create a matcher that matches the inverse of the given value. For example: before("foo", fn)
runs fn
when the action has type foo
, while before(not("foo"), fn)
runs fn
on every action except those with type foo
.
- matcher