action-chain
Why
All modern frameworks has some concept of an action. The purpose of an action is to perform side effects, this being changing the state of the application or talking to the server. How you express this action is different in the different frameworks and tools, but they typically have one thing in common... they are expressed as one function with imperative code.
There is nothing wrong with imperative code, we need it, but it has some limitations:
-
When a single function with imperative code grows it quickly becomes difficult to reason about what it does
-
There is no way to track what the function does, because it is low level and we typically point directly to other libraries and functions
-
It requires a lot of dicipline to make your code reusable and composable
action-chain moves you into a functional world by exposing a chaining API, much like RxJS. But instead of being focused on value transformation, action-chain is focused on side effects. With its "developer experience" driven implementation it allows for building developer tools that can visual all execution.
Create an action-chain
import { Action, NoValueAction, actionChainFactory, actionFactory } from 'action-chain'
const context = {
say: {
hello: () => 'hello',
goodbye: () => 'goodbye'
}
}
type Context = typeof context
const actionChain = actionChainFactory<Context>(context)
const action = function <InitialValue>(): InitialValue extends undefined
? NoValueAction<Context, InitialValue>
: Action<Context, InitialValue> {
return actionFactory<Context, InitialValue>(actionChain)
}
Define actions
const test = action<string>()
.map((name, { say }) => `${say.hello()} ${name}`)
test('Bob')
Track actions
actionChain.on('action:start', (details) => {
})
actionChain.on('operator:start', (details) => {
})
actionChain.on('operator:end', (details) => {
})
actionChain.on('action:end', (details) => {
})
Operators
do
Allows you to run effects and passes the current value a long to the next operator.
const test = action()
.do((_, { localStorage }) => {
localStorage.set('foo', 'bar')
})
map
Maps to a new value, passed to the next operator.
const test = action<string>()
.map((value) => value.toUpperCase())
try
If returning a promise, run paths based on resolved or rejected.
const test = action<string>()
.try((_, { api }) => api.getUser(), {
success: action(),
error: action()
})
when
Executes true or false path based on boolean value.
const test = action<string>()
.when((value) => value.length > 3, {
true: action(),
false: action()
})
filter
Stops execution when false.
const test = action<string>()
.filter(() => false)
.map(() => 'foo')
debounce
Debounces execution.
const test = action<string>()
.debounce(100)
.map(() => 'foo')
Extend operators
import { Action, NoValueAction, actionChainFactory, actionFactory, Execution } from 'action-chain'
interface MyAction<Context, InitialValue, Value = InitialValue>
extends MyOperators<Context, InitialValue, Value>,
Action<Context, InitialValue, Value> {}
interface NoValueMyAction<Context, InitialValue, Value = InitialValue>
extends MyOperators<Context, InitialValue, Value>,
NoValueAction<Context, InitialValue, Value> {}
interface MyOperators<Context, InitialValue, Value> {
log(): InitialValue extends undefined
? NoValueMyAction<Context, InitialValue, Value>
: MyAction<Context, InitialValue, Value>
}
function myActionFactory<Context, InitialValue, Value = InitialValue>(
actionChain: ActionChain<Context>,
initialActionId?: number,
runOperators?: (
value: any,
execution: Execution,
path: string[]
) => any | Promise<any>
): InitialValue extends undefined
? NoValueMyAction<Context, InitialValue, Value>
: MyAction<Context, InitialValue, Value> {
return Object.assign(
actionFactory<Context, InitialValue, Value>(
actionChain,
initialActionId,
runOperators
) as any,
{
log() {
const operator = (value) => {
console.log(value)
return value
}
const [
chain,
initialActionId,
runOperators,
] = this.createOperatorResult('log', '', operator)
return myActionFactory<Context, InitialValue, Value>(
chain,
initialActionId,
runOperators
)
},
}
)
}
const myAction = function<
InitialValue = undefined
>(): InitialValue extends undefined
? NoValueMyAction<Context, InitialValue>
: MyAction<Context, InitialValue> {
return myActionFactory<Context, InitialValue>(actionChain)
}