diff --git a/CHANGELOG.md b/CHANGELOG.md index c8325ee..04f3b32 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased Changes +## 3.2.0 (2023-11-17) +* Add makeThunkMiddleware to inject custom argument ([#94](https://github.com/Roblox/rodux/pull/94)). + ## 3.1.0 (2023-08-22) * Add support for devtools ([#84](https://github.com/Roblox/rodux/pull/84)) diff --git a/docs/advanced/thunks.md b/docs/advanced/thunks.md index f12a1ea..5f384af 100644 --- a/docs/advanced/thunks.md +++ b/docs/advanced/thunks.md @@ -28,4 +28,29 @@ store:dispatch(function(store) end) ``` -Thunks are a simple way to introduce more complex processing of `action` objects, but you may want to consider creating custom [`middleware`](middleware.md) for complex features instead of relying on thunks alone. \ No newline at end of file +Thunks are a simple way to introduce more complex processing of `action` objects, but you may want to consider creating custom [`middleware`](middleware.md) for complex features instead of relying on thunks alone. + +It is also possible to inject a custom argument into the thunk middleware. This is useful for cases like using an API service layer that could be swapped out for a mock service in tests. This is accomplished by using the `Rodux.makeThunkMiddleware` API instead: + +```lua +local myThunkMiddleware = Rodux.makeThunkMiddleware(myCustomArg) +local store = Rodux.Store.new(reducer, initialState, { + myThunkMiddleware, +}) + +store:dispatch(function(store, myCustomArg) + print("Hello from a thunk with extra argument:", myCustomArg) + store:dispatch({ + type = "thunkAction" + }) +end) +``` + +If multiple values need to be passed in, they can be combined into a single table: + +```lua +local myThunkMiddleware = Rodux.makeThunkMiddleware({ + [RoactNetworking] = networking, + AvatarEditorService = AvatarEditorService, +}) +``` diff --git a/docs/api-reference.md b/docs/api-reference.md index 9896928..f224ebe 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -265,3 +265,23 @@ store:dispatch(function(store) }) end) ``` + +### Rodux.makeThunkMiddleware (unreleased) +``` +Rodux.makeThunkMiddleware(extraArgument) -> thunkMiddleware +``` + +A function that creates a thunk middleware that injects a custom argument when invoking thunks (in addition to the store itself). This is useful for cases like using an API service layer that could be swapped out for a mock service in tests. + +```lua +local myThunkMiddleware = Rodux.makeThunkMiddleware(myCustomArg) +local store = Store.new(reducer, initialState, { myThunkMiddleware }) + +store:dispatch(function(store, myCustomArg) + print("Hello from a thunk with extra argument:", myCustomArg) + + store:dispatch({ + type = "thunkAction" + }) +end) +``` diff --git a/rotriever.toml b/rotriever.toml index 1e8c356..06db746 100644 --- a/rotriever.toml +++ b/rotriever.toml @@ -3,4 +3,4 @@ name = "Rodux" authors = ["Roblox"] license = "Apache-2.0" content_root = "src" -version = "3.1.0" +version = "3.2.0" diff --git a/src/init.lua b/src/init.lua index 34196e3..201bdc1 100644 --- a/src/init.lua +++ b/src/init.lua @@ -4,6 +4,7 @@ local combineReducers = require(script.combineReducers) local makeActionCreator = require(script.makeActionCreator) local loggerMiddleware = require(script.loggerMiddleware) local thunkMiddleware = require(script.thunkMiddleware) +local makeThunkMiddleware = require(script.makeThunkMiddleware) return { Store = Store, @@ -12,4 +13,5 @@ return { makeActionCreator = makeActionCreator, loggerMiddleware = loggerMiddleware.middleware, thunkMiddleware = thunkMiddleware, + makeThunkMiddleware = makeThunkMiddleware, } diff --git a/src/makeThunkMiddleware.lua b/src/makeThunkMiddleware.lua new file mode 100644 index 0000000..24c4d09 --- /dev/null +++ b/src/makeThunkMiddleware.lua @@ -0,0 +1,40 @@ +--[[ + A middleware that allows for functions to be dispatched with an extra + argument for convenience. Functions will receive two arguments: + the store itself and the extra argument provided initially to makeThunkMiddleware. + + This middleware consumes the function; middleware further down the chain + will not receive it. +]] +local function tracebackReporter(message) + return debug.traceback(message) +end + +local function makeThunkMiddleware(extraArgument) + local function thunkMiddleware(nextDispatch, store) + return function(action) + if typeof(action) == "function" then + local ok, result = xpcall(function() + return action(store, extraArgument) + end, tracebackReporter) + + if not ok then + -- report the error and move on so it's non-fatal app + store._errorReporter.reportReducerError(store:getState(), action, { + message = "Caught error in thunk", + thrownValue = result, + }) + return nil + end + + return result + end + + return nextDispatch(action) + end + end + + return thunkMiddleware +end + +return makeThunkMiddleware diff --git a/src/thunkMiddleware.lua b/src/thunkMiddleware.lua index c4db036..faca07c 100644 --- a/src/thunkMiddleware.lua +++ b/src/thunkMiddleware.lua @@ -4,31 +4,6 @@ This middleware consumes the function; middleware further down the chain will not receive it. ]] -local function tracebackReporter(message) - return debug.traceback(message) -end +local makeThunkMiddleware = require(script.Parent.makeThunkMiddleware) -local function thunkMiddleware(nextDispatch, store) - return function(action) - if typeof(action) == "function" then - local ok, result = xpcall(function() - return action(store) - end, tracebackReporter) - - if not ok then - -- report the error and move on so it's non-fatal app - store._errorReporter.reportReducerError(store:getState(), action, { - message = "Caught error in thunk", - thrownValue = result, - }) - return nil - end - - return result - end - - return nextDispatch(action) - end -end - -return thunkMiddleware +return makeThunkMiddleware(nil) -- no extra argument diff --git a/src/thunkMiddleware.spec.lua b/src/thunkMiddleware.spec.lua index 1496fbe..78e0a8f 100644 --- a/src/thunkMiddleware.spec.lua +++ b/src/thunkMiddleware.spec.lua @@ -1,6 +1,7 @@ return function() local Store = require(script.Parent.Store) local thunkMiddleware = require(script.Parent.thunkMiddleware) + local makeThunkMiddleware = require(script.Parent.makeThunkMiddleware) it("should dispatch thunks", function() local function reducer(state, action) @@ -117,4 +118,25 @@ return function() store:dispatch(safeThunk) expect(ranSafeThunk).to.equal(true) end) + + it("should send extra argument to thunks when provided", function() + local function reducer(state, action) + return state + end + + local myExtraArg = { What = "MyExtraArg" } + local store = Store.new(reducer, {}, { makeThunkMiddleware(myExtraArg) }) + local thunkCount = 0 + local extraArgParam = nil + + local function thunk(_store, extraArg) + thunkCount = thunkCount + 1 + extraArgParam = extraArg + end + + store:dispatch(thunk) + + expect(thunkCount).to.equal(1) + expect(extraArgParam).to.equal(myExtraArg) + end) end