# Core Concepts
Before we begin, let's take a look again at the Redux data flow(source unknown):
The bubbles that move around(except state) are all actions. It's not hard to tell that the design of Redux is "action centric". So to reduce boilerplate code the design of Retux is all about reusing action types.
Introducing...
# Action Catalog
import { CreateActionCatalog } from 'retux'
type ActionCatalog = CreateActionCatalog<{
ACTION0: {}
ACTION1: {
payload: boolean
}
ANOTHER_ACTION: {
payload: {
amount: number
}
meta: string
}
}>
ActionCatalog defines all the actions of a module. Since it is a basic object type, it can be split and merged which offers great flexibility.
ActionCatalog is the core of Retux. All other facilities are built surrounding ActionCatalog.
It is recommended to prefix Action names with module id. Retux exmaples use $
as separator as it is easy to be recognized between uppercase characters but you can pick whatever you like.
import { CreateActionCatalog } from 'retux'
type ActionCatalog = CreateActionCatalog<{
MODULE$ACTION0: {}
MODULE$ACTION1: {
payload: boolean
}
MODULE$ANOTHER_ACTION: {
payload: {
amount: number
}
meta: string
}
}>
# Action Type
Retux offers ActionType
for extracting action type names from ActionCatalog. As one may easily figure out, they are just keys of ActionCatalog.
import { ActionType } from 'retux'
type ModuleActionTypeNames = ActionType<ActionCatalog>
# Action
Here comes the interesting part, how can we extract actions from ActionCatalog?
It is easy to get something like:
type Actions = {
type: 'ACTION1' | 'ACTION2' | ...
payload: "action1's payload" | "action2's payload" | ...
meta: "action1's meta" | "action2's meta" | ...
}
which is not useful because we want:
if (action.type === 'ACTION1') {
action.payload // -> Will narrow down to ACTION1's payload type
}
which means we need:
type Actions =
| {
type: 'ACTION1'
payload: "action1's payload"
meta: "action1's meta"
}
| {
type: 'ACTION2'
payload: "action2's payload"
meta: "action2's meta"
}
| ...
Retux offers Action
type to deal with just that. It makes use of the "extend
on union" feature of TypeScript(>= 2.8).
You can choose basic action type { type, payload, meta }
import { Action } from 'retux'
// or
import { Action } from 'retux/lib/basic'
type AllActions = Action<ActionCatalog>
type Action1 = Action<ActionCatalog, 'ACTION1'>
Or FSA compliant (opens new window) action type { type, error, payload, meta }
import { FSA } from 'retux'
// or
import { Action } from 'retux/lib/fsa'
type AllActions = Action<ActionCatalog>
type Action1 = Action<ActionCatalog, 'ACTION1'>
Note that FSA is also strongly typed. Each action can either be
{
type: // as in ActionCatalog
error?: false
payload: // as in ActionCatalog, default none
meta: // as in ActionCatalog, default none
}
or
{
type: // as in ActionCatalog
error: true
payload: // as in ActionCatalog, default Error
meta: // as in ActionCatalog, default none
}
# Action Handlers
The default Redux architecture uses switch
in reducer which is hard for code splitting. In Retux we use simple object
instead.
const counterActionHandlers: ActionHandlers<
CounterState,
CounterActionCatalog
> = {
INCREMENT: (state, { payload }) => ({
count: state.count + (payload == null ? 1 : payload)
}),
DECREMENT: (state, { payload }) => ({
count: state.count - (payload == null ? 1 : payload)
})
}
Or with FSA:
const counterActionHandlers: ActionHandlers<
CounterState,
CounterActionCatalog
> = {
INCREMENT: (state, action) =>
action.error
? state // error handling
: {
count: state.count + (action.payload == null ? 1 : action.payload)
},
DECREMENT: (state, action) =>
action.error
? state // error handling
: {
count: state.count - (action.payload == null ? 1 : action.payload)
}
}
Notice with FSA we do not destructure the action
argument so that we can separate action types with and without error
.
Since ActionHandlers
is strongly typed, any missing or misspelled action handler will fail TypeScript compiling.
You can also get a single Action Handler type with ActionHandler
. This is useful for defining large Action Handlers outside of ActionHandlers
.
# Combine Action Handlers
There are patterns in Retux where multiple Action Handlers share the same state. In this case one may want to combine Action Handlers together.
Use combineUniqueObjects
to combine Action Handlers. It will check duplicated keys statically(yes, type checking duplicated keys) and also throw error on non-production mode.
const combinedActionHandlers = combineUniqueObjects(
actionHandlers1,
actionHandlers2,
...
)
If you want to skip checking duplicated keys, use combineObjects
.
Both of them uses Object.assign
under the hood but with type enhancement which preserves index signature of the combined Action Handlers. Do not use Object.assign
directly to join Action Handlers.
Note that there are also proxyCombineUniqueObjects
and proxyCombineObjects
which offers same API except everything is lazy loaded and cached with es6 Proxy
(fallback to combineUniqueObjects
and combineObjects
when not supported).
# Create Action Creators
Boilerplate Action Creators are generated by createActionCreators
in Retux.
const action = createActionCreators(actionHandlers)
dispatch(action.MODULE1$ACTION1())
const action = createActionCreators(actionHandlers, {
// overwrite or extends the generated Action Creators
INCREMENT: (step: number): StoreActionWithThunk<'INCREMENT'> =>
dispatch => dispatch({ type: 'INCREMENT', payload: { step } })
})
The actionHandlers
has to be of type ActionHandlers
so that createActionCreators
can infer ActionCatalog
from it and learn the types of all Actions.
Note that createActionCreators
will actually create N Action Creator functions. Retux also offers proxyActionCreators
with the same API but lazy create Action Creators on their first visits. It will fallback to createActionCreators
when es6 Proxy
is not supported.