Redux Executor
Redux enhancer for handling side effects.
Warning: API is not stable yet, will be from version 1.0
Table of Contents
- Installation
- Motivation
- Concepts
- Composition
- Narrowing
- Execution order
- API
- Code Splitting
- Typings
- License
Installation
Redux Executor requires Redux 3.1.0 or later.
npm install --save redux-executor
This assumes that you’re using npm package manager with a module bundler like Webpack or Browserify to consume UMD modules.
To enable Redux Executor, use createExecutorEnhancer
with createStore
:
;;;; const store = ;
Redux DevTools
To use Redux Executor with Redux DevTools, you have to be careful about enhancers order. It's because Redux Executor do not pass commands to next enhancers so it has to be placed after DevTools (to see commands).
const devToolsCompose = window && window__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ? window__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ : compose; const enhancer = ;
Motivation
There are many clever solutions to deal with side-effects in redux application like redux-thunk or redux-saga. The goal of this library is to be simpler than redux-saga, easier to test and more pure than redux-thunk.
Typical usage of executor is to fetch some external resource, for example list of posts. It can look like this:
;;// import action creators; // postExecutors.js { ; return postApi ;} 'FETCH_POSTS()' fetchPostsExecutor; // somwhere else in code;
So what is the difference between executor and thunk? With executors you have separation between side-effect request and side-effect call. It means that you can omit second phase and not call side-effect (if you not bind executor to the store). With this design it's very easy to write unit tests. If you use Redux DevTools it will be also easier to debug - all commands will be logged in debugger.
I recommend to use redux-executor with redux-detector library. In this combination you can for example detect if client is on given url and dispatch fetch command. All, excluding executors, will be pure.
Concepts
An Executor
It may sounds a little bit scary but there is nothing to fear - executor is very pure and simple function.
;
Like you see above, executor takes an action (called command in executors), enhanced dispatch
function and state.
It can return a Promise
to provide custom execution flow.
A Command
Command is an action with specific type
format - COMMAND_TYPE()
(like function call, instead of COMMAND_TYPE
). The idea behind
is that it's more clean to split actions to two types: normal actions (we will call it events) that tells
what has happened and commands that tells what should happen.
Events are declarative like: { type: 'USER_CLICKED_FOO' }
, commands are imperative like: { type: 'FETCH_FOO()' }
.
The reaction for event is state reduction (by reducer) which is pure, the reaction for command is executor call which is unpure.
Another thing is that events changes state (by reducer), commands not. Because of that command dispatch doesn't call store listeners (for example it doesn't re-render React application).
Composition
You can pass only one executor to the store, but with combineExecutors
and reduceExecutors
you can mix them to
one executor. For example:
;;;; // our state has shape:// {// foo: [],// bar: 1// }//// We want to bind `fooExecutor` and `anotherExecutor` to `state.foo` branch (they should run in sequence)// and also `barExecutor` to `state.bar` branch. foo: bar: barExecutor;
Mounting
To re-use executors we can also mount them to some state branch. To do this, use mountExecutor
function
with state selector and executor.
; // our state has shape:// {// foo: [1, 3],// }//// We want to bind `fooExecutor` to the length of `state.foo` branch { console; // > 2} statefoolength fooExecutor;
Narrowing
By default executor runs for every command. To limit executor to given command type, you can write if statement
or use handleCommand
/handleCommands
function.
For example to limit fooExecutor
to command FOO()
, barExecutor
to command BAR()
and mix them into one executor:
; { // foo executor logic} { // bar executor logic } // OPTION 1: handleExecutor + reduceExecutors ; // OPTION 2: handleCommands 'FOO()': fooExecutor 'BAR()': barExecutor;
Execution order
Sometimes we want to dispatch actions in proper order. To do this, we have to return promise from executors we want
to include to our execution order. If we dispatch command, dispatch method will return action (it's redux behaviour) with
additional promise
field that contains promise of our side-effects. Keep in mind that this promise is the result of
calling all combined or reduced executors (built-in implementations uses Promise.all
, see reduceExecutors,
combineExecutors). Because of that you should not rely on promise content - in fact it should
be undefined.
Lets say that we want to run firstCommand
and then secondCommand
and thirdCommand
in parallel.
The easiest solution is:
// import action creators; { return promise ;} 'FIRST_THEN_NEXT()' firstThenNextExecutor;
To be more declarative and to reduce boilerplate code, you can create generic sequenceCommandExecutor
and
parallelCommandExecutor
.
// executionFlowExecutors.js; const sequenceCommandExecutor = ; const parallelCommandExecutor = ;
With these executors we can create action creator instead of executor for the previous example.
// import action creators;; { return ;}// it will return// {// type: 'SEQUENCE()',// payload: [// { type: 'FIRST()' },// { // type: 'PARALLEL()',// payload: [// { type: 'SECOND()' },// { type: 'THIRD()' }// ]// }// ]// }
API
combineExecutors
; ;
Binds executors to state branches and combines them to one executor. Executors will be called in
sequence but promise will be resolved in parallel (by Promise.all
) Useful for re-usable executors.
createExecutorEnchancer
;; ;
Creates new redux enhancer that extends redux store api (see ExecutableStore)
ExecutableDispatch
It's type of enhanced dispatch method that can add promise
field to returned action if you dispatch command.
ExecutableStore
It's type of store that has enhanced dispatch method (see ExecutableDispatch) and
replaceExecutor
method (like replaceReducer
).
Executor
;
EXECUTOR_INIT
;
Command of this type is dispatched on init and replaceExecutor
call.
GetState
;
Simple function to get current state (we don't provide state itself because it can change during async side-effects).
handleCommand
;
Limit executor to given command type (inspired by redux-actions).
handleCommands
; ;
Similar to handleCommand
but works for multiple commands at once.
Map is an object where key is a command type, value is an executor (inspired by redux-actions).
isCommand
;
Checks if given object is a command (object.type
ends with ()
string).
isCommandType
;
Similar to isCommand
but checks only type
string.
mountExecutor
;
Mounts executor to some state branch. Useful for re-usable executors.
reduceExecutors
;
Reduces multiple executors to one. Executors will be called in sequence but promise will be resolved in parallel
(by Promise.all
). Useful for re-usable executors.
Code Splitting
Redux Executor provides replaceExecutor
method on ExecutableStore
interface (store created by Redux Executor). It's similar to
replaceReducer
- it changes executor and dispatches { type: '@@executor/INIT()' }
.
Typings
If you are using TypeScript, you don't have to install typings - they are provided in npm package.
License
MIT