Roblox/rodux

Introduce middleware

LPGhatguy opened this issue · 2 comments

Redux has the concept of middleware; it's how redux-thunk is implemented, for example.

We should introduce the same idea to Rodux.

One great concrete example is replicating events to clients and using the Rodux store as a kind of replicated data structure with limited mutation. A blessed middleware that does just this would make Rodux very attractive for managing game state I think

There are several possible ways to do this, I think.

  • Add middlewares via a method of Store, e.g. Store:addMiddleware(...).
  • Specify middlewares when creating the store as another argument, e.g. Store.new(reducer, initialState, middlewares)
  • "Replace" the store's dispatch method. This is how Redux implements middleware (it actually creates a new store with the modified dispatch method, but that's an implementation detail, I think).

Of these options, I'm most in favor of specifying middlewares at store creation. Adding them via a method seems like a potential rats' nest of errors, and replacing the store's dispatch method is...obtuse, to put it lightly. Even with Redux's very nicely written documentation, it took me a good hour or so to untangle how Redux implements middleware (...across all of 20 lines of code, for that matter). That doesn't bode well for future maintenance or contributions to the functionality, if it were replicated in Rodux.

The middleware functions themselves should have the following signature:

middleware(action, store) -> action|nil

Middlewares will be invoked for each action; they may choose to return a new action, return the existing one, or consume the action by returning nil. They are free to dispatch new actions as they see fit. This can cause re-entrancy, and care must be taken to avoid it.

I have a prototype middleware implementation available here.

I kind of diverged from my above remarks a bit. The store's dispatch method is overridden; middlewares generate new dispatch methods. They are specified in the constructor, not as a weird store enhancer like Redux.

The API now:

- function Store.new(reducer, initialState)
+ function Store.new(reducer, initialState, middlewares)

Middlewares are applied left-to-right order - the rightmost middleware is invoked first.

Middlewares look like this:

local function loggerMiddleware(next)
    return function(store, action)
        print("action dispatched:", action.type)
        next(action)
        print("new state:")
        printTable(store:getState())
    end
end

Using this looks like:

local store = Store.new(reducer, initialState, { loggerMiddleware })
store:dispatch({
    type = "someAction",
    ...
})
-- prints "action dispatched: someAction"
-- then the new state

Thoughts?