Middleware

The middleware is a Suas component that gives you the power to customize Suas dispatching logic. A middleware can be used to plug some code that gets executed after the action is dispatched to the Store and before it reaches the Reducer.

1720

Middlewares can be used to add logging capability to Suas, to perform asynchronous actions (like network calls), to store and load content from the disk, and much more.

What is a middleware

At its core a middleware is a class or struct that implements the Middleware protocol or interface:

public protocol Middleware {
  func onAction(action: Action,
                getState: @escaping GetStateFunction,
                dispatch: @escaping DispatchFunction,
                next: @escaping NextFunction)
}
interface Middleware {
    fun onAction(action: Action<*>,
                 state: GetState,
                 dispatcher: Dispatcher,
                 continuation: Continuation)
}
public interface Middleware {
    void onAction(@NonNull Action<?> action, 
            @NonNull GetState state,
            @NonNull Dispatcher dispatcher, 
            @NonNull Continuation continuation);
}

When creating the Store we can optionally pass a list of middlewares to it. Actions dispatched to the store are first sent to the middlewares. Each middleware can get the current state, dispatch additional actions, and decides whether it wants the current action to be sent to the reducer or next middleware or not.

The middleware onAction function is called with four parameters:

  • The current action that was dispatched to the Store.
  • getState: a function that when executed returns the current Store state.
  • dispatch: a function that can be used to dispatch new actions. This is the same dispatch function that the Store defines. Calling dispatch is analogous to calling store.dispatch.
  • next: a function that when executed will continue the Action processing. Calling next will execute the next middleware, or it will execute the reducer if there are no more middlewares to process.

Inside the onAction the middleware can do the following:

  • Get the current (old) state by calling getState() (before calling next)
  • Propagate the action to the next middleware (or the reducer if it's the last middleware) by calling next(action)
  • Decide to stop the action from being propagated by not calling next
  • Dispatch a new action before and after the current action. That can be done by using dispatch.
func onAction(action: Action,
  getState: @escaping GetStateFunction,
  dispatch: @escaping DispatchFunction,
  next: @escaping NextFunction) {
  
  // Action will be dispatched before the current action
  dispatch(SomePreAction())
  
  // Read the current old state. Before calling next the state is unchanged.
  let oldState = getState()
  
  // Continue the processing of the current action
  next(action)
  
  // Read the new state. After next the Store state is changed.
  let newState = getState()
  
  // Dispatch a final action after the current action
  dispatch(SomePostAction())
}
override fun onAction(action: Action<*>,
                      state: GetState,
                      dispatcher: Dispatcher,
                      continuation: Continuation) {

    // Action will be dispatched before the current action
    dispatcher.dispatch(SomePreAction())

    // Read the current old state. Before calling next the state is unchanged.
    val oldState = state.state

    // Continue the processing of the current action
    continuation.next(action)

    // Read the new state. After next the Store state is changed.
    val newState = state.state

    // Dispatch a final action after the current action
    dispatcher.dispatch(SomePostAction())
}
@Override
public void onAction(@NonNull Action<?> action,
        @NonNull GetState state,
        @NonNull Dispatcher dispatcher,
        @NonNull Continuation continuation) {

    // Action will be dispatched before the current action
    dispatcher.dispatch(new SomePreAction());

    // Read the current old state. Before calling next the state is unchanged.
    State oldState = state.getState();

    // Continue the processing of the current action
    continuation.next(action);

    // Read the new state. After next the Store state is changed.
    State newState = state.getState();

    // Dispatch a final action after the current action
    dispatcher.dispatch(new SomePostAction());
}

In order to get more accustomed to the middleware, let's implement a simple logging middleware.

Simple Logger Middleware

In this example, we will implement a simple logger middleware that logs the action receives, the old state and the new state. (Note: Suas already comes packaged with a more advanced Logger middleware).

class MyLoggerMiddleware: Middleware {
  func onAction(action: Action,
                getState: @escaping GetStateFunction,
                dispatch: @escaping DispatchFunction,
                next: @escaping NextFunction) {
    // Print the action
    print("Action receveived: \(action)")
    
    // Read the state before any reducer changes it
    print("Current state: \(getState())")
    
    // Continue the dispatching process..until the reducer reduces the action
    // Not calling `next` will prevent the action from reaching the reducer
    next(action)
    
    // Read the state after any reducer changes it
    print("New state: \(getState())")
  }
}


let store = Suas.createStore(reducer: ..., middleware: MyLoggerMiddleware())
class MyLoggerMiddleware : Middleware {

    override fun onAction(action: Action<*>,
                          state: GetState,
                          dispatcher: Dispatcher,
                          continuation: Continuation) {

        // Print the action
        print("Action receveived: " + action)

        // Read the state before any reducer changes it
        print("Current state:" + state.state)

        // Continue the dispatching process..until the reducer reduces the action
        // Not calling `next` will prevent the action from reaching the reducer
        continuation.next(action)

        // Read the state after any reducer changes it
        print("New state:" + state.state)
    }

    private fun print(message: String) {
        //...
    }
}

// ...
val store = Suas.createStore(reducers)
    .withMiddleware(MyLoggerMiddleware())
    .build()
public class MyLoggerMiddleware implements Middleware {

    @Override
    public void onAction(@NonNull Action<?> action,
            @NonNull GetState state,
            @NonNull Dispatcher dispatcher,
            @NonNull Continuation continuation) {

        // Print the action
        print("Action receveived: " + action);

        // Read the state before any reducer changes it
        print("Current state:" +  state.getState());

        // Continue the dispatching process..until the reducer reduces the action
        // Not calling `next` will prevent the action from reaching the reducer
        continuation.next(action);

        // Read the state after any reducer changes it
        print("New state:" +  state.getState());
    }

    private void print(String message) {
        //...
    }
}

// ...
Store store = Suas.createStore(reducers))
    .withMiddleware(new MyLoggerMiddleware())
    .build();

Let's break down the middleware above:

  • We print the action we received in the middleware.
  • Calling getState(), if called before calling next, returns the current state in the Store.
  • In order for the middleware to propagate the call to the next middleware, or to the reducer, we Must call next(action).
  • After calling next(action) the state in the Store is changed. Calling getState() returns the new Store state.

Using Multiple Middlewares

When creating the store you can pass multiple middlewares by combining them with the + operator in iOS, or list them separated by comma in Android.

let store = Suas.createStore(reducer: MyReducer(), 
                          middleware: MyLoggerMiddleware() + MyOtherMiddleware())
val store = Suas.createStore(MyReducer())
    .withMiddleware(MyLoggerMiddleware(), MyOtherMiddleware())
    .build()
Store store = Suas.createStore(new TodoReducer())
    .withMiddleware(new MyLoggerMiddleware(), new MyOtherMiddleware())
    .build();

When an action is dispatched, it's first sent to the MyLoggerMiddleware. In calling next(action) in MyLoggerMiddleware onAction function propagates the action to the MyOtherMiddleware. Calling next on the latter finally propagates the action to MyReducer.

If any of the middleware chooses to not call next(action) in its onAction function, the action is stopped from propagating and will not reach MyReducer.

What's Next

Suas provides three middlewares out of the box:
LoggerMiddleware to logs the action and the state.
MonitorMiddleware to monitor state changes and actions dispatched.
AsyncMiddleware to process asynchronous actions.

More advanced topics
Applications with multiple states
Async Actions
Filtering Listener
Using the StateSelector