From 7ee23111cb6f1e21176d9772c943d3626aebfc6a Mon Sep 17 00:00:00 2001 From: Tony Cuadra Date: Mon, 8 Jan 2024 10:45:34 -0800 Subject: [PATCH] Cherry-pick thunk extra-arg change onto 3.x patch branch (#94) * Add extra argument to thunk middleware (#69) This feature is equivalent to what is available in newer versions of Redux: https://github.com/reduxjs/redux-thunk#injecting-a-custom-argument It allows arbitrary parameters to injected into thunks via a table passed in at Rodux initialization time (useful for passing in services or anything else we might want to mock for tests). It can replace other custom middlewares we've written to achieve the same thing (e.g. PaginatedFetchThunk.middleware, AvatarEditorServiceThunk.middleware, RNAdapterThunk.middleware) * Make it 3.2 * oops --- CHANGELOG.md | 3 +++ docs/advanced/thunks.md | 27 +++++++++++++++++++++++- docs/api-reference.md | 20 ++++++++++++++++++ rotriever.toml | 2 +- src/init.lua | 2 ++ src/makeThunkMiddleware.lua | 40 ++++++++++++++++++++++++++++++++++++ src/thunkMiddleware.lua | 29 ++------------------------ src/thunkMiddleware.spec.lua | 22 ++++++++++++++++++++ 8 files changed, 116 insertions(+), 29 deletions(-) create mode 100644 src/makeThunkMiddleware.lua 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