Skip to content

Middleware

Abhi Muktheeswarar edited this page Jul 16, 2021 · 6 revisions
typealias Middleware<S> = (Dispatch, GetState<S>) -> (Dispatch) -> Dispatch

A middleware is a higher-order function that composes a dispatch function to return a new dispatch function. In simple words, Middleware lets you react to Action that yet to reach the actual dispatch function of a StateReserve.

The key feature of middleware is that it is composable. Multiple middlewares can be combined together, where each middleware requires no knowledge of what comes before or after it in the chain. StateReserve accepts a list of middlewares in its constructor. Please note, you can't add or remove a Middleware once it is provided to a StateReserve.

Unlike Redux, where the impure stuff like logging, IO tasks and any async tasks happen with the help of Middleware based concept redux-thunk, In Flywheel, those tasks are taken care of by SideEffect. So, Middleware in Flywheel is optional.

There are three functions available in a Middleware:

  • next(action: Action): Calling next will pass on the Action to the next Middleware.
  • dispatch(action: Action): Calling dispatch will again start the loop from the beginning. i.e, if there are 5 middlewares and if the 3rd middleware calls dispatch, again the Action will pass through all the middlewares from the beginning.
  • getState(): Provides access to current State of the StateReserve. In middleware, awaitState() suspend function is not provided, which is available in SideEffect. (awaitState() guarantees to provide a State that is updated by all the Actions in the queue.)

A simple skipMiddleware implementation that will swallow Action that implements SkipReducer.

val skipMiddleware: Middleware<State> = { dispatch, getState ->
    { next ->

        {
            if (it !is SkipReducer) {
                next(it)
            }
        }
    }
}

Instead of defining Middleware function as a variable, you also extend BaseMiddleware to create a more complex Middleware.

class CounterMiddleware(
    scope: CoroutineScope,
    dispatcherProvider: DispatcherProvider,
) : BaseMiddleware<CounterState>(scope, dispatcherProvider) {

    override fun handle(
        action: Action,
        state: GetState<CounterState>,
        next: Dispatch,
        dispatch: Dispatch,
    ) {
        when {
            action is SkipReducer -> {
                //Swallow the action
            }
            action is CounterAction.ResetAction && state().counter == 0 -> {
                //Swallow the action and dispatch a another new action
                dispatch(ShowToastAction("Counter state is already 0"))
            }
            action is CounterAction.ForceUpdateAction && action.count < 0 -> {
                //Modify the action and pass on this action
                next(action.copy(count = abs(action.count)))
            }
            else -> {
                //Pass on this action
                next(action)
            }
        }
    }
}

Use of Middleware(s)

  • If you want to swallow an Action i.e prevent an action from reaching reducer or even the StateReserve's actual dispatch function. Even if it is swallowed, hotActions will still receive the Action to broadcast to BaseHotSideEffect.
  • If you want to change the Action, For example:
    data class UpdateItemAction(val id: Int, val time: Long? = null): Action
    If you want to set the time in UpdateItemAction based on the available State in StateReserve. You can do:
    next(action.copy(time = getState().time))

But, if you rely on outside sources like IO or async task to get the time, we recommended using a SideEffect. Since any additional operation done on middleware will increase the time it takes for the Action to reach the reducer. Anything, other than the above-mentioned use cases, you will be better off with a SideEffect.

Middleware is not bound to Dispatchers.Default coroutine context. So, if a Action is dispatched from UI layer, the Middleware runs on Dispatchers.Main coroutine context or if the Action is dispatched from a SideEffect, the Middleware runs on Dispatchers.Default. Basically, it runs in the context of caller.

Rules of Middleware

  • A middleware should always call next() unless the middleware wants to swallow the Action. For example, an unauthorized Action.
  • A middleware should call next() only once.
  • A middleware can use dispatch() to send an Action to reducer. Calling dispatch() is optional.
Clone this wiki locally