diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a6f211..bdbdae1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,15 @@ # Rodux Changelog ## Current master -* No changes +* Added `combineReducers` utility, mirroring Redux's ([#9](https://github.com/Roblox/rodux/pull/9)) +* Added `createReducer` utility, similar to `redux-create-reducer` ([#10](https://github.com/Roblox/rodux/pull/10)) +* `type` is now required as a field on all actions +* Introduced middleware ([#13](https://github.com/Roblox/rodux/pull/13)) + * Thunks are no longer enabled by default, use `Rodux.thunkMiddleware` to add them back. + * Added `Rodux.loggerMiddleware` as a simple debugger + * The middleware API changed in [#29](https://github.com/Roblox/rodux/pull/29) in a backwards-incompatible way! + * Middleware now run left-to-right instead of right-to-left! +* Errors thrown in `changed` event now have correct stack traces ([#27](https://github.com/Roblox/rodux/pull/27)) -## 1.0.0 (TODO: Date) -* Initial release \ No newline at end of file +## Public Release (December 13, 2017) +* Initial release! \ No newline at end of file diff --git a/docs/api-reference.md b/docs/api-reference.md index 30c8810..b56d033 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -153,30 +153,41 @@ local reducer = createReducer(initialState, { ``` ## Middleware -Rodux provides an API that allows changing the way that actions are dispatched called *middleware*. To attach middlewares to a store, pass a list of middleware as the third argument to `Store.new`. +Rodux provides an API that allows changing the way that actions are dispatched called *middleware*. To attach middleware to a store, pass a list of middleware as the third argument to `Store.new`. + +!!! warn + The middleware API changed in [#29](https://github.com/Roblox/rodux/pull/29) -- middleware written against the old API will not work! A single middleware is just a function with the following signature: ``` -(next) -> (store, action) -> result +(nextDispatch, store) -> (action) -> result ``` -That is, middleware is a function that accepts the next middleware to apply and returns a new function. That function takes the `Store` and the current action and can dispatch more actions, log to output, or do network requests! +A middleware is a function that accepts the next dispatch function in the *middleware chain*, as well as the store the middleware is being used with, and returns a new function. That function is called whenever an action is dispatched and can dispatch more actions, log to output, or perform any side effects! A simple version of Rodux's `loggerMiddleware` is as easy as: ```lua -local function simpleLogger(next) - return function(store, action) +local function simpleLogger(nextDispatch, store) + return function(action) print("Dispatched action of type", action.type) - return next(store, action) + return nextDispatch(action) end end ``` Rodux also ships with several middleware that address common use-cases. +To apply middleware, pass a list of middleware as the third argument to `Store.new`: + +```lua +local store = Store.new(reducer, initialState, { simpleLogger }) +``` + +Middleware runs from left to right when an action is dispatched. That means that if a middleware does not call `nextDispatch` when handling an action, any middleware after it will not run. + ### Rodux.loggerMiddleware A middleware that logs actions and the new state that results from them. diff --git a/lib/Store.lua b/lib/Store.lua index 8719533..90aa02f 100644 --- a/lib/Store.lua +++ b/lib/Store.lua @@ -48,12 +48,19 @@ function Store.new(reducer, initialState, middlewares) table.insert(self._connections, connection) if middlewares then - local dispatch = Store.dispatch - for _, middleware in ipairs(middlewares) do - dispatch = middleware(dispatch) + local unboundDispatch = self.dispatch + local dispatch = function(...) + return unboundDispatch(self, ...) end - self.dispatch = dispatch + for i = #middlewares, 1, -1 do + local middleware = middlewares[i] + dispatch = middleware(dispatch, self) + end + + self.dispatch = function(self, ...) + return dispatch(...) + end end return self diff --git a/lib/Store.spec.lua b/lib/Store.spec.lua index 256b987..5e7a5fd 100644 --- a/lib/Store.spec.lua +++ b/lib/Store.spec.lua @@ -36,30 +36,93 @@ return function() end) it("should modify the dispatch method when middlewares are passed", function() + local middlewareInstantiateCount = 0 local middlewareInvokeCount = 0 + local passedDispatch + local passedStore + local passedAction local function reducer(state, action) + if action.type == "test" then + return "test state" + end + return state end - local function testMiddleware(next) - return function(store, action) + local function testMiddleware(nextDispatch, store) + middlewareInstantiateCount = middlewareInstantiateCount + 1 + passedDispatch = nextDispatch + passedStore = store + + return function(action) middlewareInvokeCount = middlewareInvokeCount + 1 - next(store, action) + passedAction = action + + nextDispatch(action) end end local store = Store.new(reducer, "initial state", { testMiddleware }) + expect(middlewareInstantiateCount).to.equal(1) + expect(middlewareInvokeCount).to.equal(0) + expect(passedDispatch).to.be.a("function") + expect(passedStore).to.equal(store) + store:dispatch({ type = "test", }) + expect(middlewareInstantiateCount).to.equal(1) expect(middlewareInvokeCount).to.equal(1) + expect(passedAction.type).to.equal("test") + + store:flush() + + expect(store:getState()).to.equal("test state") store:destruct() end) + it("should execute middleware left-to-right", function() + local events = {} + + local function reducer(state) + return state + end + + local function middlewareA(nextDispatch, store) + table.insert(events, "instantiate a") + return function(action) + table.insert(events, "execute a") + return nextDispatch(action) + end + end + + local function middlewareB(nextDispatch, store) + table.insert(events, "instantiate b") + return function(action) + table.insert(events, "execute b") + return nextDispatch(action) + end + end + + local store = Store.new(reducer, 5, { middlewareA, middlewareB }) + + expect(#events).to.equal(2) + expect(events[1]).to.equal("instantiate b") + expect(events[2]).to.equal("instantiate a") + + store:dispatch({ + type = "test", + }) + + expect(#events).to.equal(4) + expect(events[3]).to.equal("execute a") + expect(events[4]).to.equal("execute b") + end) + it("should send an initial action with a 'type' field", function() local lastAction local callCount = 0 diff --git a/lib/loggerMiddleware.lua b/lib/loggerMiddleware.lua index d3342a0..9bc922b 100644 --- a/lib/loggerMiddleware.lua +++ b/lib/loggerMiddleware.lua @@ -39,9 +39,9 @@ local loggerMiddleware = { outputFunction = print, } -function loggerMiddleware.middleware(next) - return function(store, action) - local result = next(store, action) +function loggerMiddleware.middleware(nextDispatch, store) + return function(action) + local result = nextDispatch(action) loggerMiddleware.outputFunction(("Action dispatched: %s\nState changed to: %s"):format( prettyPrint(action), diff --git a/lib/thunkMiddleware.lua b/lib/thunkMiddleware.lua index b4735e4..08c676b 100644 --- a/lib/thunkMiddleware.lua +++ b/lib/thunkMiddleware.lua @@ -4,12 +4,12 @@ This middleware consumes the function; middleware further down the chain will not receive it. ]] -local function thunkMiddleware(next) - return function(store, action) +local function thunkMiddleware(nextDispatch, store) + return function(action) if typeof(action) == "function" then return action(store) else - return next(store, action) + return nextDispatch(action) end end end