Skip to content

Commit

Permalink
Merge a5561cc into c02815b
Browse files Browse the repository at this point in the history
  • Loading branch information
AmaranthineCodices committed Mar 15, 2018
2 parents c02815b + a5561cc commit 0c3843c
Show file tree
Hide file tree
Showing 10 changed files with 570 additions and 1 deletion.
3 changes: 2 additions & 1 deletion .gitignore
@@ -1,3 +1,4 @@
/modules
/luacov.*
/installer.lua
/installer.lua
site/
158 changes: 158 additions & 0 deletions docs/api-reference.md
@@ -0,0 +1,158 @@
# Rodux API Reference

## Store
The Store class is the core piece of Rodux. It is the state container that you create and use.

### Store.new
```
Store.new(reducer, [initialState, [middlewares]]) -> Store
```

Creates and returns a new Store.

* `reducer` is the store's root reducer function, and is invokved whenever an action is dispatched. It must be a pure function.
* `initialState` is the store's initial state. This should be used to load a saved state from storage.
* `middlewares` is a list of middleware to apply to the store. See [the middleware guide](/guide/middleware.md) for more details.

The store will automatically dispatch an initialization action with a `type` of `@@INIT`.

!!! note
The initialization action does not pass through any middleware prior to reaching the reducer.

### Store.changed
```lua
Store.changed:connect(function(newState, oldState)
-- do something with newState or oldState
end)
```

A [Signal](#Signal) that is fired when the store's state is changed.

!!! danger
Do not yield within any listeners on `changed`; an error will be thrown.

### Store:dispatch
```
Store:dispatch(action) -> nil
```

Dispatches an action. The action will travel through all of the store's middlewares before reaching the store's reducer.

The action must contain a `type` field to indicate what type of action it is. No other fields are required.

### Store:getState
```
Store:getState() -> table
```

Gets the store's current state.

!!! warning
Do not modify this value. Doing so may cause serious bugs in Rodux, your code, or both!

### Store:destruct
```
Store:destruct() -> nil
```

Destroys the store, disconnecting all connections it may possess.

!!! danger
Attempting to use the store after `destruct` has been called will cause problems.

### Store:flush
```
Store:flush() -> nil
```

Flushes the store's pending actions, firing the `changed` event. You should not need to use this method; it is called automatically from within Rodux.

## Signal
The Signal class in Rodux represents a simple, predictable event that is controlled from within Rodux. It is not publicly exposed and cannot be created outside of Rodux.

### Signal:connect
```
Signal:connect(listener) -> { disconnect }
```

Connects a listener to the signal. The listener will be invoked whenever the signal is fired.

`connect` returns a table with a `disconnect` function that can be used to disconnect the listener from the signal.

## Helper functions
Rodux supplies some helper functions to make creating complex reducers easier.

### Rodux.combineReducers
A helper function that can be used to combine multiple reducers into a new reducer.

```lua
local reducer = combineReducers({
key1 = reducer1,
key2 = reducer2,
})
```

`combineReducers` is functionally equivalent to writing:

```lua
local function reducer(state, action)
return {
key1 = reducer1(state.key1, action),
key2 = reducer2(state.key2, action),
}
end
```

### Rodux.createReducer
```
Rodux.createReducer(actionHandlers) -> reducer
```

A helper function that can be used to create reducers.

Unlike JavaScript, Lua has no `switch` statement, which can make writing reducers that respond to lots of actions verbose.

```lua
local reducer = createReducer({
action1 = function(state, action)
-- Handle actions of type "action1"
end,

action2 = function(state, action)
-- Handle actions of type "action2"
end,

-- ...
})
```

## Middleware
Rodux ships with several middleware that address common use-cases.

### Rodux.loggerMiddleware
```
loggerMiddleware(outputFunction = print) -> middlewareFunction
```

A middleware that logs actions and the new state that results from them.

This middleware supports changing the output function. By default, it is `print`, and state changes are printed to the output. As a consequence of this, `loggerMiddleware` must be called when using it:

```lua
local store = Store.new(reducer, initialState, { loggerMiddleware() })
```

### Rodux.thunkMiddleware
A middleware that allows thunks to be dispatched. Thunks are functions that perform asynchronous tasks or side effects, and can dispatch actions.

```lua
local store = Store.new(reducer, initialState, { thunkMiddleware })

store:dispatch(function(store)
print("Hello from a thunk!")

store:dispatch({
type = "thunkAction",
})
end)
```
36 changes: 36 additions & 0 deletions docs/guide/actions.md
@@ -0,0 +1,36 @@
# Actions
**Actions** are bundles of data sent from your game to your store. They are tables with a single required field: `type`. The `type` field is used to describe the type of the action, which determines what it does. It is recommended, but not required, that `type` be a string, for easy debugging. You can dispatch actions to the store using its [dispatch](/api-reference.md#storedispatch) method.

Here's an example action that tells the store that a player joined:

```lua
local PLAYER_JOIN = "playerJoin"

local action = {
type = PLAYER_JOIN,
player = newPlayer,
}
```

!!! question
#### Do types have to be defined as variables first?
No, not at all! It is a good practice, however, because it is easier to change type names and to catch mistakes.

## Action creators
**Action creators** are functions that create actions for you. This allows you to validate your action arguments before the action is dispatched, add extra data to the action, and normalize your inputs. Here's an action creator for the player join action above:

```lua
local function playerJoin(player)
return {
type = PLAYER_JOIN,
player = player,
}
end
```

And here's it in use:

```lua
-- somewhere inside an event handler
store:dispatch(playerJoin(player))
```
171 changes: 171 additions & 0 deletions docs/guide/middleware.md
@@ -0,0 +1,171 @@
# Middleware
Middleware is a way to modify the behavior of a store by altering the dispatch behavior. Middleware can modify an action, consume it entirely (stopping it from being dispatched), or just do something else on top of the action. They do this by overriding the store's `dispatch` method entirely. A middleware is a factory for new `dispatch` methods, to rephrase things.

## Using middlewares
Middleware can be used by specifying them in an array in the third argument of `Store.new`:

```lua
local store = Rodux.Store.new(
reducer,
initialState,
{ middleware3, middleware2, middleware1 }
)
```

!!! question
#### Why are the middlewares in reverse order?
Rodux evaluates middleware in last-in-first-out order: the last argument is the one that's invoked first. The order of your middleware is important.

Once you've done this, the middlewares are active and will take effect whenever you use the store's `dispatch` method.

## Built-in middlewares
Rodux comes with two built-in middleware: `loggerMiddleware` and `thunkMiddleware`.

### Rodux.loggerMiddleware
`loggerMiddleware` is a very simple middleware that lets you log changes to your state. Whenever an action is dispatched, it will print two things:

* The action in its entirety
* The state after the action was reduced

To use it, specify it like this:

```lua
local store = Rodux.Store.new(
reducer,
initialState,
{ Rodux.loggerMiddleware() }
)
```

!!! question
#### Why is `loggerMiddleware` called?
`loggerMiddleware` is called because it allows you to change the function used to print to the output. For example, if you wanted to print all the changes to your store as warnings, you could do this:

```lua
local store = Rodux.Store.new(
reducer,
initialState,
{ Rodux.loggerMiddleware(warn) }
)
```

Now, whenever you dispatch an action, you'll see something like the following in the output window:

```
Action dispatched: {
type = "test"; (string)
payload = 1; (number)
}
State changed to: {
testValue = 1; (number)
}
```

### Rodux.thunkMiddleware
`thunkMiddleware` is a middleware that lets you use thunks - it lets you dispatch a function to your store, which will be run. The function can do anything, and can dispatch new actions at will. Thunks are commonly used for asynchronous, long-running operations, like reading from a data store or performing a HTTP request.

To use it, just include it in your `middlewares` table:

```lua
local store = Rodux.Store.new(
reducer,
initialState,
{ Rodux.thunkMiddleware }
)
```

Once you've done that, you can dispatch a function just like you would an action with the store's `dispatch` method:

```lua
store:dispatch(function(store)
-- Do something that takes a while

-- Then dispatch an action to tell the store about the result!
store:dispatch({
type = "someAction"
})
end)
```

## Writing your own middleware
There's nothing magic about writing middleware! Here's how you can write your own.

### A simple example: printing the type field
Here's a simple middleware that just prints the action's `type` field:

```lua
local function printType(next)
return function(store, action)
print(action.type)
next(store, action)
end
end
```

Breaking it down:

* `printType` is a function that takes one argument: `next`. This is the next middleware in the chain. At the end of the chain lies the original `dispatch` method.
* `printType` returns a new function that takes two arguments: `store` and `action`. These arguments are the *exact signature* of the original `dispatch` method.
* The function returned from `printType` prints the action's type, then calls `next` to pass the action on.

To use this function, specify it in the third argument to `Store.new`:

```lua
local function reducer(state, action)
-- Just return the same state, for demonstrational purposes.
return state
end

local store = Store.new(reducer, {}, { printType })

store:dispatch({
type = "testAction"
})
```

Run this code and you'll see this in the output:
```
testAction
```

### Canceling actions
Nothing says you *have* to call `next` at all! Here's a middleware that just swallows up any action that it comes across. These actions never modify the store's state.

```lua
local function swallowAction(next)
return function(store, action)
-- Do nothing! Since next is not called, the action never moves on.
end
end
```

### Modifying actions: PascalCased Type
Similarly, you don't always have to call `next` with the same action. Say you prefer using `PascalCase` for your actions. Rodux requires that your actions have a `type` field, so your code style is being broken! Middlewares to the rescue - you can replace the action so that it fits the structure Rodux is expecting, without having to make compromises about your casing.

```lua
local function pascalCaseType(next)
return function(store, action)
-- If the action has a Type field, substitute it with an identical action
-- that substitutes type for Type!
if action.Type then
local newAction = {}

for key, value in pairs(action) do
-- Change the casing on the Type field
if key == "Type" then
newAction.type = value
-- Everything else can stay as-is
else
newAction[key] = value
end
end

-- Pass the new action on!
next(store, newAction)
-- Otherwise, just send the action on!
else
next(store, action)
end
end
end
```

0 comments on commit 0c3843c

Please sign in to comment.