├── .babelrc ├── .gitignore ├── .travis.yml ├── README.md ├── examples └── bank-accounts │ ├── README.md │ ├── config │ ├── .eslintrc │ ├── env.js │ ├── jest │ │ ├── cssTransform.js │ │ └── fileTransform.js │ ├── paths.js │ ├── polyfills.js │ ├── webpack.config.dev.js │ └── webpack.config.prod.js │ ├── package.json │ ├── public │ ├── favicon.ico │ └── index.html │ ├── scripts │ ├── build.js │ ├── start.js │ └── test.js │ ├── src │ ├── App.js │ ├── actions.js │ ├── components │ │ ├── Accounts.js │ │ ├── Form.js │ │ ├── Log.js │ │ ├── accounts.css │ │ ├── form.css │ │ └── log.css │ ├── hocs │ │ └── withAnimations.js │ ├── index.css │ ├── index.js │ ├── reducer.js │ ├── selectors.js │ ├── server-mock.js │ └── store.js │ └── yarn.lock ├── index.d.ts ├── lib ├── dsl.js ├── enhancer.js ├── free.js ├── index.js ├── interpreters.js └── utils.js ├── package.json ├── src ├── dsl.js ├── enhancer.js ├── free.js ├── index.js ├── interpreters.js └── utils.js ├── test ├── application │ ├── actions.js │ ├── index.js │ ├── reducer.js │ ├── server-mock.js │ ├── transaction.js │ └── utils.js ├── dsl.test.js ├── free.test.js └── store.test.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015" 4 | ], 5 | "plugins" : [ 6 | "transform-object-rest-spread" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | 6 | # testing 7 | coverage 8 | 9 | # production 10 | build 11 | 12 | # misc 13 | .DS_Store 14 | .env 15 | npm-debug.log* 16 | yarn-debug.log* 17 | yarn-error.log* 18 | 19 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "6" 4 | 5 | cache: yarn 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # redux-free-flow 2 | 3 | [![Build Status](https://travis-ci.org/yiransheng/redux-free-flow.svg?branch=master)](https://travis-ci.org/yiransheng/redux-free-flow) 4 | 5 | # What it does 6 | 7 | It let's you write pure, composable and versatile redux store interactions (both sync and async) like this: 8 | 9 | ```javascript 10 | import { Do, dispatch, effect, end, rollback } from "redux-free-flow"; 11 | import { withdraw, deposit, readBalance } from "./account-actions"; 12 | import { callApiToTransfer } from "./api-utils"; 13 | 14 | export function transferMoney(fromAccount, toAccount, amount) { 15 | return Do(function*() { 16 | const balance = yield readBalance(fromAccount); 17 | if (balance >= amount) { 18 | // sync dispatch 19 | yield withdraw(fromAccount, amount); 20 | yield deposit(toAccount, amount); 21 | // async and effectual 22 | const response = yield effect( 23 | callApiToTransfer.bind(null, fromAccount, toAccount, amount)); 24 | if (response === "success") { 25 | yield end; 26 | } else { 27 | yield rollback; 28 | } 29 | } else { 30 | yield dispatch({ type: "ERROR_INSUFFICIENT_FUNDS" }); 31 | } 32 | }); 33 | } 34 | // later 35 | store.dispatch(transferMoney(21, 23, 1000)); 36 | ``` 37 | 38 | Yes, `dispatch({ type: "ERROR_INSUFFICIENT_FUNDS" })` is pure - it does not dispatch actions, instead it's an expression evaluates to some data structure to be interpreted later (when dispatched by `redux` store). This small snippet of code does the following once dispatched: 39 | 40 | * Use `getState` (by `readBalance`) to get account balance of `fromAccount` 41 | * Check if the account has enough funds to transfer 42 | * If so, dispatches two actions, `withdraw` and `deposit` atomically 43 | * makes an api call to tell the server this transfer transaction has happened 44 | * conclude everything if api response indicates a server-side success 45 | * otherwise, rollback the entire transaction 46 | * If not enough funds, dispatches an error action `{ type: "ERROR_INSUFFICIENT_FUNDS" }` 47 | 48 | # Example 49 | 50 | [Demo Link](https://gusty-houses.surge.sh) 51 | 52 | [Source](./examples/bank-accounts) 53 | 54 | # How it Works 55 | 56 | The inspirations of this enhancer are: 57 | 58 | * Free Monad, DSL / Interpreter pattern 59 | * Event Sourcing Systems 60 | * `redux-saga` `co` `async-await` etc. 61 | * transactional semantics 62 | 63 | `dispatch`, `effect`, `rollback` etc are all functions that return data structures which encodes the shape and control flow of effectful computations (`redux` store api calls). In fact, the underlying data structure here is a Free Monad. If you don't know what it is don't worry, for the purpose of using these library, they are just immutable objects with `then` method. For example: 64 | 65 | ```javascript 66 | const callDispatch = dispatch({ type: "SOME_ACTION" }); 67 | const twiceDispatch = callDispatch.then(() => { 68 | return callDispatch; 69 | }); 70 | ``` 71 | 72 | You'd write these store interactions just like how you would write promise chains - and the final interaction when dispatched will do almost exactly what you think it does. 73 | 74 | Furthermore, a handy generator oriented helper `Do` is provided to make code more readable. `Haskell` has `do` notation, `Scala` has `for` comprehension, and javaScript has `co` and `async-wait` syntax for flattening out nested promise chains. `Do` lets you use `generator`s to rewrite the code above to: 75 | 76 | ```javascript 77 | const twiceDispatch = () => Do(function* () { 78 | yield callDispatch; 79 | yield callDispatch; 80 | }); 81 | ``` 82 | 83 | * Note: `Do(function* { .. })` is no-longer pure as `generators` are stateful, making the resulting interactions interpretable **only once**. It is better to wrap `Do` calls inside a function, and every time you dispatch the same interaction, call the function again for a new iterator. 84 | 85 | ## Comparisons with Other Libraries 86 | 87 | * Isn't this like `redux-thunk`? 88 | 89 | Kinda. `redux-thunks` gives you the freedom to consult `getState` and do multiple and possibly async `dispatch` calls. But `thunks` are effectful and not composable. You cannot easily take the return value of a `thunk` and use it as inputs of other `thunks` easily, nor can you rollback `thunk` actions without big changes to reducer. This library gives you all the raw power of `redux-thunk` but non of the effectful nastiness... 90 | 91 | * `redux-saga` 92 | 93 | Think an DSL / interaction / free monad (I haven't come up a name yet...) as a one-time saga. It doesn't have access to actions from other dispatch calls, but the general idea is similar: declarative and composable async control flows. 94 | 95 | * `redux-loop` 96 | 97 | DSL / Interactions / free monads are move expressive than `redux-loop` `Effects / Cmds`. They incorporate both read, sync dispatch and async effects in one unified framework. Oh, and rollbacks... 98 | 99 | # API 100 | 101 | Installation and Integration: 102 | 103 | ```shell 104 | yarn add redux-free-flow 105 | ``` 106 | 107 | ```javascript 108 | import enhancer from "redux-free-flow"; 109 | 110 | const store = createStore(reducer, preloadedState, enhancer); 111 | // or 112 | const store = compose(...otherEnhancers, enhancer)(createStore)(reducer, preloadedState) 113 | ``` 114 | 115 | ** Note: This library assumes all actions are plain objects, it will not work with other middlewares and enhancers that gives you the ability to use non-plain-object actions (such as `redux-thunk`, `redux-promise`) . In other words, an `action` must be something you `reducer` can understand. 116 | 117 | Core apis: 118 | 119 | * `read` 120 | * `dispatch` 121 | * `effect` 122 | * `end` 123 | * `rollback` 124 | 125 | Don't worry too much about the type signature. 126 | 127 | ### read 128 | 129 | ```javascript 130 | read(select : State -> A) : Free DSL A 131 | ``` 132 | 133 | Given a `select` function, `read` produces a command to read value from store, "returned" value is `select(store.getState())`, the value can be accessed like: 134 | 135 | ``` 136 | read(select) 137 | .then(currentValue => { .. }) 138 | ``` 139 | 140 | ### dispatch 141 | 142 | ```javascript 143 | dispatch(action: PlainObject) : Free DSL Unit 144 | ``` 145 | 146 | `dispatch` takes an plain object (action), and returns a command that dispatches the action when executed. 147 | 148 | For example: 149 | 150 | ``` 151 | store.dispatch( 152 | read(state => state.counter) 153 | .then(counter => { 154 | if (counter > 0) { 155 | return dispatch({ type: DECREMENT }); 156 | } 157 | return end; 158 | }) 159 | ) 160 | ``` 161 | 162 | This will dispatch an `DECREMENT` action only if store's `counter` value is positive. `redux` takes `dispatch` out of action creators from `flux`, now I am putting it back in... 163 | 164 | ### effect 165 | 166 | ``` 167 | effect(promiseFactory: () -> Promise A) : Free DSL A 168 | ``` 169 | 170 | `effect` takes a function that returns a promise and stores it. When interpreted, the function gets called, and when the promise it returns resolves, the value becomes available to the next `then` function in the DSL chain. 171 | 172 | ### end 173 | 174 | ``` 175 | end :: Free DSL Unit 176 | ``` 177 | 178 | The termination of a transaction, `end` is automatically assumed for any chain of commands, if the last one is not `end`. 179 | 180 | ### rollback 181 | 182 | ``` 183 | rollback :: Free DSL Unit 184 | ``` 185 | 186 | The most magical of them all. When encountered by the internal interpreter, all dispatched action from the given transaction so far will be taken out history as if they never happened! Other concurrent actions (and transactions) remain intact. 187 | 188 | 189 | 190 | # Performance 191 | 192 | Dispatching a free monad switches redux store to a "event sourcing" mode. What it means is all actions dispatched (including vanilla ones) gets added to a queue (this is why `rollback` is possible). If you have a long running or perpetual thing going on like this: 193 | 194 | ```javascript 195 | const forever = () => Do(function* () { 196 | yield effect(timeout(1000)); 197 | yield forever(); 198 | }) 199 | ``` 200 | 201 | You'd either running into a infinite loop (if everything is sync) or a async loop (if `effect` is used like above) with memory leak from the action queue. 202 | 203 | # Is it production ready? 204 | 205 | ## No. 206 | 207 | It's a complex piece of machinery, there for sure are bugs. Also lots of edge case handling code is missing. 208 | 209 | -------------------------------------------------------------------------------- /examples/bank-accounts/README.md: -------------------------------------------------------------------------------- 1 | # Example: Bank Account 2 | 3 | -------------------------------------------------------------------------------- /examples/bank-accounts/config/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint:recommended", 3 | "env": { 4 | "browser": true, 5 | "commonjs": true, 6 | "node": true, 7 | "es6": true 8 | }, 9 | "parserOptions": { 10 | "ecmaVersion": 6 11 | }, 12 | "rules": { 13 | "no-console": "off", 14 | "strict": ["error", "global"] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/bank-accounts/config/env.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Grab NODE_ENV and REACT_APP_* environment variables and prepare them to be 4 | // injected into the application via DefinePlugin in Webpack configuration. 5 | 6 | var REACT_APP = /^REACT_APP_/i; 7 | 8 | function getClientEnvironment(publicUrl) { 9 | var raw = Object 10 | .keys(process.env) 11 | .filter(key => REACT_APP.test(key)) 12 | .reduce((env, key) => { 13 | env[key] = process.env[key]; 14 | return env; 15 | }, { 16 | // Useful for determining whether we’re running in production mode. 17 | // Most importantly, it switches React into the correct mode. 18 | 'NODE_ENV': process.env.NODE_ENV || 'development', 19 | // Useful for resolving the correct path to static assets in `public`. 20 | // For example, . 21 | // This should only be used as an escape hatch. Normally you would put 22 | // images into the `src` and `import` them in code to get their paths. 23 | 'PUBLIC_URL': publicUrl 24 | }); 25 | // Stringify all values so we can feed into Webpack DefinePlugin 26 | var stringified = { 27 | 'process.env': Object 28 | .keys(raw) 29 | .reduce((env, key) => { 30 | env[key] = JSON.stringify(raw[key]); 31 | return env; 32 | }, {}) 33 | }; 34 | 35 | return { raw, stringified }; 36 | } 37 | 38 | module.exports = getClientEnvironment; 39 | -------------------------------------------------------------------------------- /examples/bank-accounts/config/jest/cssTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // This is a custom Jest transformer turning style imports into empty objects. 4 | // http://facebook.github.io/jest/docs/tutorial-webpack.html 5 | 6 | module.exports = { 7 | process() { 8 | return 'module.exports = {};'; 9 | }, 10 | getCacheKey() { 11 | // The output is always the same. 12 | return 'cssTransform'; 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /examples/bank-accounts/config/jest/fileTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | 5 | // This is a custom Jest transformer turning file imports into filenames. 6 | // http://facebook.github.io/jest/docs/tutorial-webpack.html 7 | 8 | module.exports = { 9 | process(src, filename) { 10 | return 'module.exports = ' + JSON.stringify(path.basename(filename)) + ';'; 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /examples/bank-accounts/config/paths.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var path = require('path'); 4 | var fs = require('fs'); 5 | var url = require('url'); 6 | 7 | // Make sure any symlinks in the project folder are resolved: 8 | // https://github.com/facebookincubator/create-react-app/issues/637 9 | var appDirectory = fs.realpathSync(process.cwd()); 10 | function resolveApp(relativePath) { 11 | return path.resolve(appDirectory, relativePath); 12 | } 13 | 14 | // We support resolving modules according to `NODE_PATH`. 15 | // This lets you use absolute paths in imports inside large monorepos: 16 | // https://github.com/facebookincubator/create-react-app/issues/253. 17 | 18 | // It works similar to `NODE_PATH` in Node itself: 19 | // https://nodejs.org/api/modules.html#modules_loading_from_the_global_folders 20 | 21 | // We will export `nodePaths` as an array of absolute paths. 22 | // It will then be used by Webpack configs. 23 | // Jest doesn’t need this because it already handles `NODE_PATH` out of the box. 24 | 25 | // Note that unlike in Node, only *relative* paths from `NODE_PATH` are honored. 26 | // Otherwise, we risk importing Node.js core modules into an app instead of Webpack shims. 27 | // https://github.com/facebookincubator/create-react-app/issues/1023#issuecomment-265344421 28 | 29 | var nodePaths = (process.env.NODE_PATH || '') 30 | .split(process.platform === 'win32' ? ';' : ':') 31 | .filter(Boolean) 32 | .filter(folder => !path.isAbsolute(folder)) 33 | .map(resolveApp); 34 | 35 | var envPublicUrl = process.env.PUBLIC_URL; 36 | 37 | function ensureSlash(path, needsSlash) { 38 | var hasSlash = path.endsWith('/'); 39 | if (hasSlash && !needsSlash) { 40 | return path.substr(path, path.length - 1); 41 | } else if (!hasSlash && needsSlash) { 42 | return path + '/'; 43 | } else { 44 | return path; 45 | } 46 | } 47 | 48 | function getPublicUrl(appPackageJson) { 49 | return envPublicUrl || require(appPackageJson).homepage; 50 | } 51 | 52 | // We use `PUBLIC_URL` environment variable or "homepage" field to infer 53 | // "public path" at which the app is served. 54 | // Webpack needs to know it to put the right