Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add createAction #35

Merged
merged 6 commits into from Dec 3, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -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 (2017-12-13)
* Initial release!
50 changes: 50 additions & 0 deletions docs/api-reference.md
Expand Up @@ -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`.

Expand Down
30 changes: 29 additions & 1 deletion docs/introduction/actions.md
Expand Up @@ -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`.
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!
2 changes: 2 additions & 0 deletions src/init.lua
@@ -1,13 +1,15 @@
local Store = require(script.Store)
local createReducer = require(script.createReducer)
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,
combineReducers = combineReducers,
makeActionCreator = makeActionCreator,
loggerMiddleware = loggerMiddleware.middleware,
thunkMiddleware = thunkMiddleware,
}
24 changes: 24 additions & 0 deletions src/makeActionCreator.lua
@@ -0,0 +1,24 @@
--[[
A helper function to define a Rodux action creator with an associated name.
]]
local function makeActionCreator(name, fn)
assert(type(name) == "string", "Bad argument #1: Expected a 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
69 changes: 69 additions & 0 deletions src/makeActionCreator.spec.lua
@@ -0,0 +1,69 @@
return function()
local makeActionCreator = require(script.Parent.makeActionCreator)

it("should set the name of the actionCreator creator", function()
local FooAction = makeActionCreator("foo", function()
return {}
end)

expect(FooAction.name).to.equal("foo")
end)

it("should return a table when called as a function", function()
local FooAction = makeActionCreator("foo", function()
return {}
end)

expect(FooAction()).to.be.a("table")
end)

it("should set the type of the action creator", function()
local FooAction = makeActionCreator("foo", function()
return {}
end)

expect(FooAction().type).to.equal("foo")
end)

it("should set values", function()
local FooAction = makeActionCreator("foo", function(value)
return {
value = value
}
end)

expect(FooAction(100).value).to.equal(100)
end)

it("should throw when its result does not return a table", function()
local FooAction = makeActionCreator("foo", function()
return function() end
end)

expect(FooAction).to.throw()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
expect(FooAction).to.throw()
expect(FooAction).to.throw("must return a table")

nvm this is using a 3yr old version of test ez

end)

it("should throw if the first argument is not a string", function()
expect(function()
makeActionCreator(nil, function()
return {}
end)
end).to.throw()

expect(function()
makeActionCreator(100, function()
return {}
end)
end).to.throw()
end)

it("should throw if the second argument is not a function", function()
expect(function()
makeActionCreator("foo", nil)
end).to.throw()

expect(function()
makeActionCreator("foo", {})
end).to.throw()
end)
end