From 1ad2a3b677d9aa0e863a5b6fec0fbde4ba44b9b8 Mon Sep 17 00:00:00 2001 From: Paul Doyle Date: Tue, 18 Sep 2018 14:19:37 -0700 Subject: [PATCH 1/5] Add createAction --- lib/createAction.lua | 45 +++++++++++++++++++++++ lib/createAction.spec.lua | 77 +++++++++++++++++++++++++++++++++++++++ lib/init.lua | 2 + 3 files changed, 124 insertions(+) create mode 100644 lib/createAction.lua create mode 100644 lib/createAction.spec.lua diff --git a/lib/createAction.lua b/lib/createAction.lua new file mode 100644 index 0000000..8ced073 --- /dev/null +++ b/lib/createAction.lua @@ -0,0 +1,45 @@ +--[[ + A helper function to define a Rodux action creator with an associated name. + + `createAction` provides a utility that makes action creation cleaner + and less error prone. Define your Rodux action like this: + + return Action("MyAction", function(value) + return { + value = value, + } + end) + + The `type` field will be added automatically. Additionally, the returned action + creator now has a 'name' property that can be checked by your reducer: + + local MyAction = require(Reducers.MyAction) + ... + if action.type == MyAction.name then + -- change some state! + end + + This creates a clear link between reducers and the actions they use, and allows + linters like Luacheck to generate warnings if we type names incorrectly. +]] + +local function createAction(name, fn) + assert(type(name) == "string", "A name must be provided to create an Action") + assert(type(fn) == "function", "A function must be provided to create an Action") + + return setmetatable({ + name = name, + }, { + __call = function(self, ...) + local result = fn(...) + + assert(type(result) == "table", "An action must return a table") + + result.type = name + + return result + end + }) +end + +return createAction \ No newline at end of file diff --git a/lib/createAction.spec.lua b/lib/createAction.spec.lua new file mode 100644 index 0000000..073b192 --- /dev/null +++ b/lib/createAction.spec.lua @@ -0,0 +1,77 @@ +return function() + local createAction = require(script.Parent.createAction) + + it("should set the name of the action", function() + local action = createAction("foo", function() + return {} + end) + + expect(action.name).to.equal("foo") + end) + + it("should be able to be called as a function", function() + local action = createAction("foo", function() + return {} + end) + + expect(action).never.to.throw() + end) + + it("should return a table when called as a function", function() + local action = createAction("foo", function() + return {} + end) + + expect(action()).to.be.a("table") + end) + + it("should set the type of the action", function() + local action = createAction("foo", function() + return {} + end) + + expect(action().type).to.equal("foo") + end) + + it("should set values", function() + local action = createAction("foo", function(value) + return { + value = value + } + end) + + expect(action(100).value).to.equal(100) + end) + + it("should throw when its result does not return a table", function() + local action = createAction("foo", function() + return function() end + end) + + expect(action).to.throw() + end) + + it("should throw if the first argument is not a string", function() + expect(function() + createAction(nil, function() + return {} + end) + end).to.throw() + + expect(function() + createAction(100, function() + return {} + end) + end).to.throw() + end) + + it("should throw if the second argument is not a function", function() + expect(function() + createAction("foo", nil) + end).to.throw() + + expect(function() + createAction("foo", {}) + end).to.throw() + end) +end \ No newline at end of file diff --git a/lib/init.lua b/lib/init.lua index acef1df..63ef321 100644 --- a/lib/init.lua +++ b/lib/init.lua @@ -1,5 +1,6 @@ local Store = require(script.Store) local createReducer = require(script.createReducer) +local createAction = require(script.createAction) local combineReducers = require(script.combineReducers) local loggerMiddleware = require(script.loggerMiddleware) local thunkMiddleware = require(script.thunkMiddleware) @@ -7,6 +8,7 @@ local thunkMiddleware = require(script.thunkMiddleware) return { Store = Store, createReducer = createReducer, + createAction = createAction, combineReducers = combineReducers, loggerMiddleware = loggerMiddleware.middleware, thunkMiddleware = thunkMiddleware, From 073b00e95d7327b4f84a80d6c59928a7067be9d8 Mon Sep 17 00:00:00 2001 From: Paul Doyle Date: Wed, 10 Oct 2018 10:07:27 -0700 Subject: [PATCH 2/5] Rename, update wording --- lib/createAction.lua | 45 ------------------ lib/makeActionCreator.lua | 46 +++++++++++++++++++ ...on.spec.lua => makeActionCreator.spec.lua} | 0 3 files changed, 46 insertions(+), 45 deletions(-) delete mode 100644 lib/createAction.lua create mode 100644 lib/makeActionCreator.lua rename lib/{createAction.spec.lua => makeActionCreator.spec.lua} (100%) diff --git a/lib/createAction.lua b/lib/createAction.lua deleted file mode 100644 index 8ced073..0000000 --- a/lib/createAction.lua +++ /dev/null @@ -1,45 +0,0 @@ ---[[ - A helper function to define a Rodux action creator with an associated name. - - `createAction` provides a utility that makes action creation cleaner - and less error prone. Define your Rodux action like this: - - return Action("MyAction", function(value) - return { - value = value, - } - end) - - The `type` field will be added automatically. Additionally, the returned action - creator now has a 'name' property that can be checked by your reducer: - - local MyAction = require(Reducers.MyAction) - ... - if action.type == MyAction.name then - -- change some state! - end - - This creates a clear link between reducers and the actions they use, and allows - linters like Luacheck to generate warnings if we type names incorrectly. -]] - -local function createAction(name, fn) - assert(type(name) == "string", "A name must be provided to create an Action") - assert(type(fn) == "function", "A function must be provided to create an Action") - - return setmetatable({ - name = name, - }, { - __call = function(self, ...) - local result = fn(...) - - assert(type(result) == "table", "An action must return a table") - - result.type = name - - return result - end - }) -end - -return createAction \ No newline at end of file diff --git a/lib/makeActionCreator.lua b/lib/makeActionCreator.lua new file mode 100644 index 0000000..835adb2 --- /dev/null +++ b/lib/makeActionCreator.lua @@ -0,0 +1,46 @@ +--[[ + A helper function to define a Rodux action creator with an associated name. + + `makeActionCreator` provides a utility that makes action creation cleaner + and less error prone. Define your Rodux action like this: + + return makeActionCreator("MyAction", function(value) + return { + value = value, + } + end) + + The resulting action creator will populate the `type` field will be automatically. + Additionally, the action creatoralso has a 'name' property that can be checked by + your reducer: + + local MyAction = require(Reducers.MyAction) + ... + if action.type == MyAction.name then + -- change some state! + end + + This creates a clear link between reducers and the actions they use, and allows + linters like Luacheck to generate warnings if we type names incorrectly. +]] + +local function makeActionCreator(name, fn) + assert(type(name) == "string", "Bad argument #1: Expecteda string name for the action creator") + assert(type(fn) == "function", "Bad argument #2: Expected a function that creates action objects") + + return setmetatable({ + name = name, + }, { + __call = function(self, ...) + local result = fn(...) + + assert(type(result) == "table", "Invalid action: An action creator must return a table") + + result.type = name + + return result + end + }) +end + +return makeActionCreator \ No newline at end of file diff --git a/lib/createAction.spec.lua b/lib/makeActionCreator.spec.lua similarity index 100% rename from lib/createAction.spec.lua rename to lib/makeActionCreator.spec.lua From 98160ec3c3c48c988e94d84866a49b9363215d8c Mon Sep 17 00:00:00 2001 From: Paul Doyle Date: Wed, 10 Oct 2018 10:45:16 -0700 Subject: [PATCH 3/5] Prune comments and add docs --- docs/api-reference.md | 50 ++++++++++++++++++++++++++++++++++++ docs/introduction/actions.md | 30 +++++++++++++++++++++- lib/makeActionCreator.lua | 23 ----------------- 3 files changed, 79 insertions(+), 24 deletions(-) diff --git a/docs/api-reference.md b/docs/api-reference.md index b7c27f3..3d26a0e 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -152,6 +152,56 @@ local reducer = createReducer(initialState, { }) ``` +### Rodux.makeActionCreator +``` +Rodux.makeActionCreator(name, actionGeneratorFunction) -> actionCreator +``` + +A helper function that can be used to make action creators. + +Action creators are helper objects that will generate actions from provided data and automatically populate the `type` field. + +Actions often have a structure that looks like this: + +```lua +local MyAction = { + type = "SetFoo", + value = 1, +} +``` + +They are often generated by functions that take the action's data as arguments: + +```lua +local function SetFoo(value) + return { + type = "SetFoo", + value = value, + } +end +``` + +`makeActionCreator` looks similar, but it automatically populates the action's type with the action creator's name. This makes it easier to keep track of which actions your reducers are responding to: + +Make an action creator in `SetFoo.lua`: +```lua +return makeActionCreator("SetFoo", function(value) + -- The action creator will automatically add the 'type' field + return { + value = value, + } +end) +``` + +Then check for that action by name in `FooReducer.lua`: +```lua +local SetFoo = require(SetFoo) +... +if action.type == SetFoo.name then + -- change some state! +end +``` + ## Middleware 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`. diff --git a/docs/introduction/actions.md b/docs/introduction/actions.md index e212878..e750d91 100644 --- a/docs/introduction/actions.md +++ b/docs/introduction/actions.md @@ -22,4 +22,32 @@ store:dispatch(ReceivedNewPhoneNumber("15552345678")) ``` !!! info - In most cases your `action` will be sent directly to the `reducer` to be processed. However, if you specified any `middleware` when initializing your `store`, your `action` might also be processed by that `middleware`. \ No newline at end of file + In most cases your `action` will be sent directly to the `reducer` to be processed. However, if you specified any `middleware` when initializing your `store`, your `action` might also be processed by that `middleware`. + +Additionally, Rodux provides a helper method called `makeActionCreator` to generate 'action creators'. These are a lot like the `ReceivedNewPhoneNumber` function above, except for two key differences: + +* Instead of functions, action creators returned from `makeActionCreator` are callable tables that also include a `name` field. +* Action creators will automatically populate the `type` field of each action they create using their `name`. + +We can define an action creator like this: + +```lua +return makeActionCreator("ReceivedNewPhoneNumber", function(phoneNumber) + return { + phoneNumber = phoneNumber, + } +end) +``` + +Since the `name` of the action creator populates the `type` of the actions it creates, we can use an action creators `name` to identify actions that were created by it. As we'll see in the Reducers section, this is helpful for determining which action we're processing: + +```lua + local MyAction = require(MyAction) + ... + if action.type == MyAction.name then + -- change some state! + end +``` + +!!! info + Actions are nothing more than tables with a `type` field, so there are many ways to generate them! If `makeActionCreator` doesn't work for your project, you can always generate actions and action creators however you like! \ No newline at end of file diff --git a/lib/makeActionCreator.lua b/lib/makeActionCreator.lua index 835adb2..94f283b 100644 --- a/lib/makeActionCreator.lua +++ b/lib/makeActionCreator.lua @@ -1,29 +1,6 @@ --[[ A helper function to define a Rodux action creator with an associated name. - - `makeActionCreator` provides a utility that makes action creation cleaner - and less error prone. Define your Rodux action like this: - - return makeActionCreator("MyAction", function(value) - return { - value = value, - } - end) - - The resulting action creator will populate the `type` field will be automatically. - Additionally, the action creatoralso has a 'name' property that can be checked by - your reducer: - - local MyAction = require(Reducers.MyAction) - ... - if action.type == MyAction.name then - -- change some state! - end - - This creates a clear link between reducers and the actions they use, and allows - linters like Luacheck to generate warnings if we type names incorrectly. ]] - local function makeActionCreator(name, fn) assert(type(name) == "string", "Bad argument #1: Expecteda string name for the action creator") assert(type(fn) == "function", "Bad argument #2: Expected a function that creates action objects") From ae9899a843deaa6dd4a1708a347ab6e459cf0ecb Mon Sep 17 00:00:00 2001 From: Paul Doyle Date: Wed, 10 Oct 2018 10:49:11 -0700 Subject: [PATCH 4/5] Fix tests, fix naming, make wording in tests more consistent --- lib/init.lua | 4 ++-- lib/makeActionCreator.spec.lua | 42 ++++++++++++++-------------------- 2 files changed, 19 insertions(+), 27 deletions(-) diff --git a/lib/init.lua b/lib/init.lua index 63ef321..34196e3 100644 --- a/lib/init.lua +++ b/lib/init.lua @@ -1,15 +1,15 @@ local Store = require(script.Store) local createReducer = require(script.createReducer) -local createAction = require(script.createAction) local combineReducers = require(script.combineReducers) +local makeActionCreator = require(script.makeActionCreator) local loggerMiddleware = require(script.loggerMiddleware) local thunkMiddleware = require(script.thunkMiddleware) return { Store = Store, createReducer = createReducer, - createAction = createAction, combineReducers = combineReducers, + makeActionCreator = makeActionCreator, loggerMiddleware = loggerMiddleware.middleware, thunkMiddleware = thunkMiddleware, } diff --git a/lib/makeActionCreator.spec.lua b/lib/makeActionCreator.spec.lua index 073b192..a67a917 100644 --- a/lib/makeActionCreator.spec.lua +++ b/lib/makeActionCreator.spec.lua @@ -1,65 +1,57 @@ return function() - local createAction = require(script.Parent.createAction) + local makeActionCreator = require(script.Parent.makeActionCreator) - it("should set the name of the action", function() - local action = createAction("foo", function() + it("should set the name of the actionCreator creator", function() + local FooAction = makeActionCreator("foo", function() return {} end) - expect(action.name).to.equal("foo") - end) - - it("should be able to be called as a function", function() - local action = createAction("foo", function() - return {} - end) - - expect(action).never.to.throw() + expect(FooAction.name).to.equal("foo") end) it("should return a table when called as a function", function() - local action = createAction("foo", function() + local FooAction = makeActionCreator("foo", function() return {} end) - expect(action()).to.be.a("table") + expect(FooAction()).to.be.a("table") end) - it("should set the type of the action", function() - local action = createAction("foo", function() + it("should set the type of the action creator", function() + local FooAction = makeActionCreator("foo", function() return {} end) - expect(action().type).to.equal("foo") + expect(FooAction().type).to.equal("foo") end) it("should set values", function() - local action = createAction("foo", function(value) + local FooAction = makeActionCreator("foo", function(value) return { value = value } end) - expect(action(100).value).to.equal(100) + expect(FooAction(100).value).to.equal(100) end) it("should throw when its result does not return a table", function() - local action = createAction("foo", function() + local FooAction = makeActionCreator("foo", function() return function() end end) - expect(action).to.throw() + expect(FooAction).to.throw() end) it("should throw if the first argument is not a string", function() expect(function() - createAction(nil, function() + makeActionCreator(nil, function() return {} end) end).to.throw() expect(function() - createAction(100, function() + makeActionCreator(100, function() return {} end) end).to.throw() @@ -67,11 +59,11 @@ return function() it("should throw if the second argument is not a function", function() expect(function() - createAction("foo", nil) + makeActionCreator("foo", nil) end).to.throw() expect(function() - createAction("foo", {}) + makeActionCreator("foo", {}) end).to.throw() end) end \ No newline at end of file From 3602ec5da90b017d3b4d211c2896e24f2c2e4315 Mon Sep 17 00:00:00 2001 From: Paul Doyle Date: Wed, 10 Oct 2018 10:56:47 -0700 Subject: [PATCH 5/5] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index effb45c..00d26d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ * 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)) * Fixed `createReducer` having incorrect behavior with `nil` state values ([#33](https://github.com/Roblox/rodux/pull/33)) +* Added `makeActionCreator` utility for common action creator pattern ([#35](https://github.com/Roblox/rodux/pull/35)) ## Public Release (December 13, 2017) * Initial release! \ No newline at end of file