EffectMiddleware

@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
public final class EffectMiddleware<InputActionType, OutputActionType, StateType, Dependencies> : MiddlewareProtocol
extension EffectMiddleware: Semigroup
extension EffectMiddleware: Monoid where Dependencies == Void

Easiest way to implement a Middleware, with a single function that gives you all you need, and from which you can return an Effect.

A MiddlewareEffect is a function providing an incoming Action, State and Context (dispatcher source, dependencies, cancellation closure) and expecting as result one or multiple effects that will eventually result in outgoing actions.

An Effect is a publisher or observable type according to your reactive framework. It can be a one-shot effect, such as an HTTP request, an observation that gives back multiple values over time, such as CoreLocation or NotificationCenter, a Timer, a pure value already known or even an empty effect using the .doNothing constructor, if there’s no need for side-effects. Effect cannot fail, and its element/output/value type is the OutputAction generic of this middleware. The purpose is running tasks, creating actions as they respond and returning these actions over time back to the Store. When the Effect completes, it should send a completion event so the middleware will cleanup the resources.

Cancellation: effects can optionally provide a cancellation token, which can be any hashable value. If another effect arrives in the same middleware instance having the very same cancellation token, the previous effect will be cancelled and replaced by the new one. This is useful in case you want to keep only the last request of certain kind running, but cancel any previous ongoing request when a new is dispatched. You can also explicitly cancel one or many effects at any point. For that, you will be given a toCancel closure during every action arrival within the Context (third parameter). Feel free to call cancellation at that point or even later, if you hold this toCancel closure.

Examples

Using Promises

let someMiddleware = EffectMiddleware<ApiRequestAction, ApiResponseAction, SomeState, Void>.onAction { action, state, context in
  switch action {
  case .users:
    return .promise { completion in
      DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
        completion(ApiResponseAction.someResponse("42"))
      }
    }
  case .somethingIDontCare:
    return .doNothing
  }
}

From Publisher

typealias ApiFetchMiddlewareDependencies = (session: @escaping () -> URLSession, decoder: @escaping () -> JSONDecoder)

let apiFetchMiddleware =
  EffectMiddleware<ApiRequestAction, ApiResponseAction, SomeState, ApiFetchMiddlewareDependencies>.onAction { action, state, context in
    switch action {
    case .users:
      return context.dependencies.urlSession
        .dataTaskPublisher(for: fetchAllUsersURL())
        .map { data, _ in data }
        .decode(type: [User].self, decoder: context.dependencies)
        .map { users in ApiResponseAction.gotUserList(users) }
        .replaceError(with: ApiResponseAction.errorRetrivingUserList)
        .asEffect
    case .user(id: UUID):
      // ..
    case .somethingIDontCare:
      return .doNothing
    }
  }.inject((session: { URLSession.shared }, decoder: JSONDecoder.init))

Cancellation

typealias ApiFetchMiddlewareDependencies = (session: @escaping () -> URLSession, decoder: @escaping () -> JSONDecoder)

let apiFetchMiddleware =
  EffectMiddleware<ApiRequestAction, ApiResponseAction, SomeState, ApiFetchMiddlewareDependencies>.onAction { action, state, context in
    switch action {
    case let .userPicture(userId):
      return context.dependencies.urlSession
        .dataTaskPublisher(for: fetchPicture())
        .map { data, _ in ApiResponseAction.gotUserPicture(id: userId, data: data) }
        .replaceError(with: ApiResponseAction.errorRetrivingUserPicture(id: userId))
        .asEffect(cancellationToken: "image-for-user-\(userId)") // this will automatically cancel any pending download for the same image
                                                                 // using the URL would also be possible
    case let .cancelImageDownload(userId):
      return context.toCancel("image-for-user-\(userId)")        // alternatively you can explicitly cancel tasks by token
    }
  }.inject((session: { URLSession.shared }, decoder: JSONDecoder.init))
  • Declaration

    Swift

    public func handle(action: InputActionType, from dispatcher: ActionSource, state: @escaping GetState<StateType>) -> IO<OutputActionType>
  • Undocumented

    Declaration

    Swift

    public func lift<GlobalInputActionType, GlobalOutputActionType, GlobalStateType>(
        inputAction inputActionMap: @escaping (GlobalInputActionType) -> InputActionType?,
        outputAction outputActionMap: @escaping (OutputActionType) -> GlobalOutputActionType,
        state stateMap: @escaping (GlobalStateType) -> StateType
    ) -> EffectMiddleware<GlobalInputActionType, GlobalOutputActionType, GlobalStateType, Dependencies>
  • Undocumented

    Declaration

    Swift

    public func lift<GlobalInputActionType, GlobalOutputActionType>(
        inputAction inputActionMap: @escaping (GlobalInputActionType) -> InputActionType?,
        outputAction outputActionMap: @escaping (OutputActionType) -> GlobalOutputActionType
    ) -> EffectMiddleware<GlobalInputActionType, GlobalOutputActionType, StateType, Dependencies>
  • Undocumented

    Declaration

    Swift

    public func lift<GlobalInputActionType, GlobalStateType>(
        inputAction inputActionMap: @escaping (GlobalInputActionType) -> InputActionType?,
        state stateMap: @escaping (GlobalStateType) -> StateType
    ) -> EffectMiddleware<GlobalInputActionType, OutputActionType, GlobalStateType, Dependencies>
  • Undocumented

    Declaration

    Swift

    public func lift<GlobalOutputActionType, GlobalStateType>(
        outputAction outputActionMap: @escaping (OutputActionType) -> GlobalOutputActionType,
        state stateMap: @escaping (GlobalStateType) -> StateType
    ) -> EffectMiddleware<InputActionType, GlobalOutputActionType, GlobalStateType, Dependencies>
  • Undocumented

    Declaration

    Swift

    public func lift<GlobalInputActionType>(
        inputAction inputActionMap: @escaping (GlobalInputActionType) -> InputActionType?
    ) -> EffectMiddleware<GlobalInputActionType, OutputActionType, StateType, Dependencies>
  • Undocumented

    Declaration

    Swift

    public func lift<GlobalOutputActionType>(
        outputAction outputActionMap: @escaping (OutputActionType) -> GlobalOutputActionType
    ) -> EffectMiddleware<InputActionType, GlobalOutputActionType, StateType, Dependencies>
  • Undocumented

    Declaration

    Swift

    public func lift<GlobalStateType>(
        state stateMap: @escaping (GlobalStateType) -> StateType
    ) -> EffectMiddleware<InputActionType, OutputActionType, GlobalStateType, Dependencies>
  • Undocumented

    Declaration

    Swift

    public static func onAction(
        do onAction: @escaping (InputActionType, ActionSource, @escaping GetState<StateType>) -> Effect<Dependencies, OutputActionType>
    ) -> MiddlewareReader<Dependencies, EffectMiddleware>
  • Declaration

    Swift

    public static func <> (lhs: EffectMiddleware, rhs: EffectMiddleware) -> EffectMiddleware

Available where InputActionType == OutputActionType

  • Undocumented

    Declaration

    Swift

    public func lift<GlobalActionType, GlobalStateType>(
        action actionMap: WritableKeyPath<GlobalActionType, InputActionType?>,
        state stateMap: KeyPath<GlobalStateType, StateType>
    ) -> EffectMiddleware<GlobalActionType, GlobalActionType, GlobalStateType, Dependencies>

Available where StateType: Identifiable

  • Undocumented

    Declaration

    Swift

    public func liftToCollection<GlobalAction, GlobalState, CollectionState: MutableCollection>(
        inputAction actionMap: @escaping (GlobalAction) -> ElementIDAction<StateType.ID, InputActionType>?,
        outputAction outputMap: @escaping (ElementIDAction<StateType.ID, OutputActionType>) -> GlobalAction,
        stateCollection: @escaping (GlobalState) -> CollectionState
    ) -> EffectMiddleware<GlobalAction, GlobalAction, GlobalState, Dependencies> where CollectionState.Element == StateType

Available where StateType: Identifiable, InputActionType == OutputActionType

  • Undocumented

    Declaration

    Swift

    public func liftToCollection<GlobalAction, GlobalState, CollectionState: MutableCollection>(
        action actionMap: WritableKeyPath<GlobalAction, ElementIDAction<StateType.ID, InputActionType>?>,
        stateCollection: KeyPath<GlobalState, CollectionState>
    ) -> EffectMiddleware<GlobalAction, GlobalAction, GlobalState, Dependencies> where CollectionState.Element == StateType

Available where Dependencies == Void

  • Undocumented

    Declaration

    Swift

    public static func onAction(
        do onAction: @escaping (InputActionType, ActionSource, @escaping GetState<StateType>) -> Effect<Dependencies, OutputActionType>
    ) -> EffectMiddleware<InputActionType, OutputActionType, StateType, Dependencies>
  • Declaration

    Swift

    public static var identity: EffectMiddleware<InputActionType, OutputActionType, StateType, Dependencies> { get }