Middleware
@available(*, deprecated, message: "Use `MiddlewareProtocol` instead of `Middleware`. This protocol will be removed on 1.0.")
public protocol Middleware : MiddlewareProtocol
⛓ Middleware
is a plugin, or a composition of several plugins, that are assigned to the ReduxStoreProtocol
pipeline in order to handle each
action received (InputActionType
), to execute side-effects in response, and eventually dispatch more actions (OutputActionType
) in the process.
This happens before the Reducer
to do its job.
We can think of a Middleware as an object that transforms actions into sync or async tasks and create more actions as these side-effects complete, also being able to check the current state at any point.
An action is a lightweight structure, typically an enum, that is dispatched into the ActionHandler
(usually a StoreProtocol
).
A Store like ReduxStoreProtocol
enqueues a new action that arrives and submits it to a pipeline of middlewares. So, in other words, a Middleware
is class that handles actions, and has the power to dispatch more actions to the ActionHandler
chain. The Middleware
can also simply ignore the
action, or it can execute side-effects in response, such as logging into file or over the network, or execute http requests, for example. In case of
those async tasks, when they complete the middleware can dispatch new actions containing a payload with the response (a JSON file, an array of
movies, credentials, etc). Other middlewares will handle that, or maybe even the same middleware in a future RunLoop, or perhaps some Reducer
, as
reducers pipeline is at the end of every middleware pipeline.
Middlewares can schedule a callback to be executed after the reducer pipeline is done mutating the global state. At that point, the middleware will have access to the new state, and in case it cached the old state it can compare them, log, audit, perform analytics tracking, telemetry or state sync with external devices, such as Apple Watches. Remote Debugging over the network is also a great use of a Middleware.
Every action dispatched also comes with its action source, which is the primary dispatcher of that action. Middlewares can access the file, line, function and additional information about the entity responsible for creating and dispatching that action, which is a very powerful debugging information that can help developers to trace how the information flows through the app.
Because the Middleware
receive all actions and accesses the state of the app at any point, anything can be done from these small and reusable
boxes. For example, the same CoreLocation
middleware could be used from an iOS app, its extensions, the Apple Watch extension or even different
apps, as long as they share some sub-state struct.
Some suggestions of middlewares:
- Run Timers, pooling some external resource or updating some local state at a constant time
- Subscribe for
CoreData
,Realm
,Firebase Realtime Database
or equivalent database changes - Be a
CoreLocation
delegate, checking for significant location changes or beacon ranges and triggering actions to update the state - Be a
HealthKit
delegate to track activities, or even combining that withCoreLocation
observation in order to track the activity route - Logger, Telemetry, Auditing, Analytics tracker, Crash report breadcrumbs
- Monitoring or debugging tools, like external apps to monitor the state and actions remotely from a different device
WatchConnectivity
sync, keep iOS and watchOS state in sync- API calls and other “cold observables”
- Network Reachability
- Navigation through the app (Redux Coordinator pattern)
CoreBluetooth
central or peripheral managerCoreNFC
manager and delegateNotificationCenter
and other delegates- WebSocket, TCP Socket, Multipeer and many other connectivity protocols
RxSwift
observables,ReactiveSwift
signal producers,Combine
publishers- Observation of traits changes, device rotation, language/locale, dark mode, dynamic fonts, background/foreground state
- Any side-effect, I/O, networking, sensors, third-party libraries that you want to abstract
┌─────┐ ┌─────┐
│ │ handle ┌──────────┐ request ┌ ─ ─ ─ ─ response ┌──────────┐ dispatch │ │
│ │ ┌─────────▶│Middleware├─────────────▶ External│─────────────▶│Middleware│───────────▶│Store│─ ─ ▶ ...
│ │ │ Action │ Pipeline │ side-effects │ World side-effects │ callback │ New Action │ │
│ │ │ └──────────┘ ─ ─ ─ ─ ┘ └──────────┘ └─────┘
┌──────┐ dispatch │ │ │ ▲
│Button│─────────▶│Store│──▶│ └───afterReducer─────┐ ┌────────┐
└──────┘ Action │ │ │ │ ┌─▶│ View 1 │
│ │ │ ┌─────┐ │ └────────┘
│ │ │ reduce ┌──────────┐ │ │ onNext │ ┌────────┐
│ │ └─────────▶│ Reducer ├───────────▶│Store│────────────▶├─▶│ View 2 │
│ │ Action │ Pipeline │ New state │ │ New state │ └────────┘
└─────┘ + └──────────┘ └─────┘ │ ┌────────┐
State └─▶│ View 3 │
└────────┘
Middleware protocol is generic over 3 associated types:
InputActionType:
The Action type that this Middleware
knows how to handle, so the store will forward actions of this type to this middleware.
Thanks to optics, this action can be a sub-action lifted to a global action type in order to compose with other middlewares acting on the global action of an app. Please check lift(inputActionMap:outputActionMap:stateMap:)
for more details.
OutputActionType:
The Action type that this Middleware
will eventually trigger back to the store in response of side-effects. This can be the same as InputActionType
or different, in case you want to separate your enum in requests and responses.
Thanks to optics, this action can be a sub-action lifted to a global action type in order to compose with other middlewares acting on the global action of an app. Please check lift(inputActionMap:outputActionMap:stateMap:)
for more details.
StateType:
The State part that this Middleware
needs to read in order to make decisions. This middleware will be able to read the most up-to-date StateType
from the store at any point in time, but it can never write or make changes to it. In some cases, middleware don’t need reading the whole global state, so we can decide to allow only a sub-state, or maybe this middleware doesn’t need to read any state, so the StateType
can safely be set to Void
.
Thanks to lenses, this state can be a sub-state lifted to a global state in order to compose with other middlewares acting on the global state of an app. Please check lift(inputActionMap:outputActionMap:stateMap:)
for more details.
When implementing your Middleware, all you have to do is to handle the incoming actions:
When implementing your Middleware, all you have to do is to handle the incoming actions:
class LoggerMiddleware: Middleware {
typealias InputActionType = AppGlobalAction // It wants to receive all possible app actions
typealias OutputActionType = Never // No action is generated from this Middleware
typealias StateType = AppGlobalState // It wants to read the whole app state
var getState: GetState<AppGlobalState>!
func receiveContext(getState: @escaping GetState<AppGlobalState>, output: AnyActionHandler<Never>) {
self.getState = getState
}
func handle(action: AppGlobalAction, from dispatcher: ActionSource, afterReducer: inout AfterReducer) {
let stateBefore: AppGlobalState = getState()
let dateBefore = Date()
afterReducer = .do {
let stateAfter = self.getState()
let dateAfter = Date()
let source = "\(dispatcher.file):\(dispatcher.line) - \(dispatcher.function) | \(dispatcher.info ?? "")"
Logger.log(action: action, from: source, before: stateBefore, after: stateAfter, dateBefore: dateBefore, dateAfter: dateAfter)
}
}
}
class FavoritesAPIMiddleware: Middleware {
typealias InputActionType = FavoritesAction // It wants to receive only actions related to Favorites
typealias OutputActionType = FavoritesAction // It wants to also dispatch actions related to Favorites
typealias StateType = FavoritesModel // It wants to read the app state that manages favorites
var getState: GetState<FavoritesModel>!
var output: AnyActionHandler<FavoritesAction>!
func receiveContext(getState: @escaping GetState<FavoritesModel>, output: AnyActionHandler<FavoritesAction>) {
self.getState = getState
self.output = output
}
func handle(action: FavoritesAction, from dispatcher: ActionSource, afterReducer: inout AfterReducer) {
guard let .toggleFavorite(movieId) = action else { return }
let favoritesList = getState()
let makeFavorite = !favoritesList.contains(where: { $0.id == movieId })
API.changeFavorite(id: movieId, makeFavorite: makeFavorite) (completion: { result in
switch result {
case let .success(value):
self.output.dispatch(.changedFavorite(movieId, isFavorite: true), info: "API.changeFavorite callback")
case let .failure(error):
self.output.dispatch(.changedFavoriteHasFailed(movieId, isFavorite: false, error: error), info: "API.changeFavorite callback")
}
})
}
}
-
Handles the incoming actions and may or not start async tasks, check the latest state at any point or dispatch additional actions. This is also a good place for analytics, tracking, logging and telemetry. You can schedule tasks to run after the reducer changed the global state if you want, and/or execute things before the reducer. This function is only called by the store after the
receiveContext(getState:output:)
was called, so if you saved the received context from there you can safely use it here to get the state or dispatch new actions. Setting theafterReducer
in/out parameter is optional, if you don’t set it, it defaults to.doNothing()
.Declaration
Swift
func handle(action: InputActionType, from dispatcher: ActionSource, afterReducer: inout AfterReducer)
Parameters
action
the action to be handled
dispatcher
information about the action source, representing the entity that created and dispatched the action
afterReducer
it can be set to perform any operation after the reducer has changed the global state. If the function ends before you set this in/out parameter,
afterReducer
will default to.doNothing()
. -
handle(action:
Extension methodfrom: state: ) Declaration
Swift
public func handle(action: InputActionType, from dispatcher: ActionSource, state: @escaping GetState<StateType>) -> IO<OutputActionType>