├── .babelrc ├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── .travis.yml ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── examples └── navigation-react-redux │ ├── .babelrc │ ├── .gitignore │ ├── README.md │ ├── actions │ └── index.js │ ├── components │ ├── Repos.js │ ├── UserSearchInput.js │ └── UserSearchResults.js │ ├── constants │ └── ActionTypes.js │ ├── containers │ ├── Admin.js │ ├── App.js │ ├── ReposByUser.js │ ├── Root.js │ └── UserSearch.js │ ├── epics │ ├── adminAccess.js │ ├── clearSearchResults.js │ ├── fetchReposByUser.js │ ├── index.js │ ├── searchUsers.js │ ├── searchUsersDebounced.js │ └── stateStreamTest.js │ ├── index.html │ ├── index.js │ ├── package.json │ ├── reducers │ ├── adminAccess.js │ ├── index.js │ ├── reposByUser.js │ ├── searchInFlight.js │ └── userResults.js │ ├── store │ └── index.js │ ├── utils │ └── index.js │ ├── webpack.config.dev.babel.js │ ├── webpack.config.prod.babel.js │ └── yarn.lock ├── index.d.ts ├── package.json ├── src ├── actions.js ├── combineEpics.js ├── constants.js ├── createEpicMiddleware.js ├── createStateStreamEnhancer.js ├── index.js ├── select.js ├── selectArray.js └── withState.js ├── tests ├── combineEpics.test.js ├── select.test.js └── selectArray.test.js ├── webpack.config.babel.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "cjs": { 4 | "presets": [ 5 | ["env", { 6 | "modules": "commonjs" 7 | }], 8 | "stage-3" 9 | ] 10 | }, 11 | "es": { 12 | "presets": [ 13 | ["env", { 14 | "modules": false 15 | }], 16 | "stage-3" 17 | ] 18 | }, 19 | "umd": { 20 | "presets": [ 21 | ["env", { 22 | "modules": false 23 | }], 24 | "stage-3" 25 | ] 26 | }, 27 | "test": { 28 | "presets": [ 29 | ["env", { 30 | "modules": "commonjs" 31 | }], 32 | "stage-3" 33 | ] 34 | } 35 | }, 36 | "comments": false 37 | } -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # /node_modules and /bower_components ignored by default 2 | 3 | dist/ 4 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "node": true, 5 | "es6": true 6 | }, 7 | 8 | "parserOptions": { 9 | "ecmaFeatures": { 10 | "experimentalObjectRestSpread": true, 11 | "jsx": true 12 | } 13 | }, 14 | 15 | "plugins": [ 16 | "react", 17 | "better", 18 | "fp", 19 | "import", 20 | "promise", 21 | "standard" 22 | ], 23 | 24 | "extends": ["standard-pure-fp", "standard-react"], 25 | 26 | "rules": { 27 | // Allow dangling commas for better clarity in diffs 28 | "comma-dangle": [2, "always-multiline"], 29 | 30 | // ES6 Rules 31 | "arrow-parens": [2, "as-needed"], 32 | "prefer-arrow-callback": 2, 33 | 34 | // Relax fp rules for library internals & more common react code in example 35 | "better/explicit-return": 0, 36 | "better/no-ifs": 0, 37 | "fp/no-rest-parameters": 0, 38 | "better/no-new": 0, 39 | "fp/no-throw": 0, 40 | "fp/no-this": 0, 41 | "fp/no-class": 0, 42 | "fp/no-mutation": 0, 43 | "fp/no-nil": 0, 44 | "fp/no-unused-expression": 0, 45 | "fp/no-mutating-methods": 0, 46 | 47 | // Extra React rules not provided by standard-react 48 | "react/react-in-jsx-scope": 2, 49 | "jsx-quotes": [2, "prefer-single"], 50 | // Disable propTypes validation 51 | "react/prop-types": 0 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | npm-debug.log 2 | node_modules 3 | lib 4 | es 5 | temp 6 | dist 7 | _book 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "node" 4 | 5 | script: 6 | - npm run safety-check 7 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Josh Burgess 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | redux-most 2 | ========== 3 | 4 | [Most.js](https://github.com/cujojs/most) based middleware for [Redux](http://redux.js.org/). 5 | 6 | Handle async actions with monadic streams & reactive programming. 7 | 8 | ### [Jump to API Reference](https://github.com/joshburgess/redux-most#api-reference) 9 | 10 | ### Install 11 | With yarn (recommended): 12 | ```bash 13 | yarn add redux-most 14 | ``` 15 | 16 | or with npm: 17 | ```bash 18 | npm install --save redux-most 19 | ``` 20 | 21 | Additionally, make sure the peer dependencies, `redux` and `most`, are also installed. 22 | 23 | 24 | ### Background 25 | 26 | `redux-most` is based on [`redux-observable`](https://redux-observable.js.org/). 27 | It uses the same pattern/concept of ["epics"](https://redux-observable.js.org/docs/basics/Epics.html) 28 | without requiring [`RxJS 5`](http://reactivex.io/rxjs/) as a peer dependency. 29 | Although `redux-observable` does provide capability for using other stream libraries via adapters, 30 | `redux-most` allows you to bypass needing to install both `RxJS 5` and `Most`. I prefer `Most` for 31 | working with observables and would rather have minimal dependencies. So, I wrote 32 | this middleware primarily for my own use. 33 | 34 | Please, see `redux-observable`'s [documentation](https://redux-observable.js.org/) 35 | for details on usage. 36 | 37 | ### Why Most over RxJS? 38 | 39 | `RxJS 5` is great. It's quite a bit faster than `RxJS 4`, and `Rx`, in general, is a 40 | very useful tool which happens to exist across many different languages. 41 | Learning it is definitely a good idea. However, `Most` is significantly smaller, 42 | less complicated, and faster than `RxJS 5`. I prefer its more minimal set of 43 | operators and its focus on performance. Also, like [`Ramda`](http://ramdajs.com/) 44 | or [`lodash/fp`](https://github.com/lodash/lodash/wiki/FP-Guide), `Most` 45 | supports a functional API in which the data collection (a stream, rather than 46 | an array, in this case) gets passed in last. This is important, because it 47 | allows you to use functional programming techniques like currying & partial 48 | application, which you can't do with `RxJS` without writing your own wrapper 49 | functions, because it only offers an OOP/fluent/chaining style API. 50 | 51 | ### Why integrate `Most`/`RxJS` with `redux` instead of recreating it with streams? 52 | 53 | It's true that it's quite easy to implement the core ideas of `Redux` with 54 | observables using the `scan` operator. (See my [inferno-most-fp-demo](https://github.com/joshburgess/inferno-most-fp-demo) 55 | for an example.) However, the [Redux DevTools](https://github.com/gaearon/redux-devtools) 56 | provide what is arguably the nicest developer tooling experience currently available 57 | in the JavaScript ecosystem. Therefore, it is huge to be able to maintain it as an asset 58 | while still reaping the benefits of reactive programming with streams. Purists, those who 59 | are very experienced with working with observables, and those working on smaller apps 60 | may not care as much about taking advantage of that tooling as using an elegant 61 | streams-only based solution, and that's fine. The important thing is having a choice. 62 | 63 | ### Why `redux-most` or `redux-observable` over [`redux-saga`](https://redux-saga.js.org/)? 64 | 65 | `redux-saga` is nice. It's a sophisticated approach to handling asynchronous 66 | actions with `Redux` and can handle very complicated tasks with ease. However, 67 | due to generators being pull-based, it is much more imperative in nature. I 68 | simply prefer the more declarative style of push-based streams & reactive 69 | programming. 70 | 71 | ### Differences between `redux-most` & `redux-observable` 72 | 73 | __Summary__ 74 | 75 | - There are no adapters. `redux-most` is only intended to be used with `Most`. 76 | - `redux-most` offers 2 separate APIs: a `redux-observable`-like API, where Epics 77 | get passed an action stream & a store middleware object containing `dispatch` & `getState` 78 | methods, and a stricter, more declarative API, where Epics get passed an action stream & a state stream. 79 | - `combineEpics` takes in an array of epics instead of multiple arguments. 80 | - Standard `Most` streams are used instead of a custom Observable extension. 81 | - `select` and `selectArray` are available instead of the variadic `ofType`. 82 | 83 | 84 | __Further Elaboration:__ 85 | 86 | As the name implies, `redux-most` does not offer adapters for use with other reactive 87 | programming libraries that implement the Observable type. It's merely an implementation of 88 | `redux-observable`'s "Epic" pattern exclusively intended for use with `Most`. `Most` is arguably 89 | the fastest, simplest, most functional, & most elegant reactive programming library in the 90 | JavaScript ecosystem right now, and `Most 2.0` will be even better, as it will feature an 91 | auto-curried API like `lodash/fp` and `ramda`, but for working with streams instead of arrays. 92 | For a preview of what's to come, check out what's going on [here](https://github.com/mostjs/core). 93 | 94 | Initially, `redux-most` offered the same API as `redux-observable`, where Epics received an action 95 | stream & a store middleware object containing `dispatch` & `getState` methods. However, it now offers 96 | both that API and another stricter, more declarative API which eliminates the use of `dispatch` & 97 | `getState`. The reason for this is that I rarely found myself using the imperative `dispatch` 98 | method. It's not really needed, because you can use `switch`, `merge`, `mergeArray`, etc. to send 99 | multiple actions through your outgoing stream. This is nice, because it allows you to stay locked into 100 | the declarative programming style the entire time. 101 | 102 | However, using `getState` was still required in epics that needed access to the current state. I 103 | wanted a nice, convenient way to access the current state, just like I had for dispatching actions. 104 | So, I created an alternate API where Epics receive a stream of state changes rather than the 105 | `{ dispatch, getState }` object. This state stream, combined with the new `withState` utility function, 106 | let's you use streams for both dispatching actions & accessing the current state, allowing you to stay 107 | focused & in the zone (the reactive programming mindset). 108 | 109 | Moving on, whereas `comebineEpics` is variadic in `redux-observable`, it's unary in `redux-most`. It 110 | takes in only one argument, an array of epics, instead of individual epics getting passed in as separate 111 | arguments. 112 | 113 | As for streams, I chose not to extend the `Observable` type with a custom `ActionsObservable` 114 | type. So, when working with `redux-most`, you will be working with normal `most` 115 | streams without any special extension methods. However, I have offered something 116 | similar to `redux-observable`'s `ofType` operator in `redux-most` with the 117 | `select` and `selectArray` helper functions. 118 | 119 | 120 | Like `ofType`, `select` and `selectArray` are convenience utilities for filtering 121 | actions by a specific type or types. In `redux-observable`, `ofType` can optionally take multiple 122 | action types to filter on. In `redux-most`, we want to be more explicit, as it is generally a good 123 | practice in functional programming to prefer a known number of arguments over a variable amount 124 | of arguments. Therefore, `select` is used when we want to filter by a single action type, and 125 | `selectArray` is used when we want to filter by multiple action types (via an array) simultaneously. 126 | 127 | Additionally, to better align with the `Most` API, and because these functions take a known number 128 | of arguments, `select` & `selectArray` are curried, which allows them to be used in either a 129 | fluent style or a more functional style which enables the use of further currying, partial 130 | application, & functional composition. 131 | 132 | To use the fluent style, just use `Most`'s `thru` operator to pass the stream 133 | through to `select`/`selectArray` as the 2nd argument. 134 | 135 | ```js 136 | // Fluent style 137 | const filteredAction$ = action$.thru(select(SOME_ACTION_TYPE)) 138 | const filteredActions$ = action$.thru(selectArray([SOME_ACTION_TYPE, SOME_OTHER_ACTION_TYPE])) 139 | ``` 140 | 141 | Otherwise, simply directly pass the stream as the 2nd argument. 142 | 143 | ```js 144 | // Functional style 145 | const filteredAction$ = select(SOME_ACTION_TYPE, action$) 146 | const filteredActions$ = selectArray([SOME_ACTION_TYPE, SOME_OTHER_ACTION_TYPE], action$) 147 | ``` 148 | Alternatively, you can delay passing the 2nd argument while defining functional pipelines 149 | via functional composition by using the `compose` or `pipe` functions from your favorite FP library, 150 | like `ramda` or `lodash/fp`. Again, this is because `select` & `selectArray` are auto-curried. Being 151 | able to program in this very functional & Pointfree style is one of the main reasons why someone 152 | might prefer using redux-most over redux-observable. 153 | 154 | ```js 155 | // Functional & Pointfree style using currying & functional composition 156 | import { compose, curry, pipe } from 'ramda' 157 | import { debounce, filter, map } from 'most' 158 | 159 | // NOTE: Most 2.0 will feature auto-curried functions, but right now we must curry them manually. 160 | const curriedDebounce = curry(debounce) 161 | const curriedFilter = curry(filter) 162 | const curriedMap = curry(map) 163 | 164 | // someEpic is a new function which is still awaiting one argument, the action$ 165 | const someEpic = compose( 166 | curriedMap(someFunction), 167 | curriedDebounce(800), 168 | select(SOME_ACTION_TYPE) 169 | ) 170 | 171 | // someOtherEpic is a new function which is still awaiting one argument, the action$ 172 | // pipe is the same as compose, but read from left-to-right rather than right-to-left. 173 | const someOtherEpic = pipe( 174 | selectArray([SOME_ACTION_TYPE, SOME_OTHER_ACTION_TYPE]), 175 | curriedFilter(somePredicate), 176 | curriedMap(someFunction) 177 | ) 178 | ``` 179 | 180 | ## API Reference 181 | 182 | - [createEpicMiddleware](https://github.com/joshburgess/redux-most#createepicmiddleware-rootepic) 183 | - [createStateStreamEnhancer](https://github.com/joshburgess/redux-most#createstatestreamenhancer-epicmiddleware) 184 | - [combineEpics](https://github.com/joshburgess/redux-most#combineepics-epics) 185 | - [EpicMiddleware](https://github.com/joshburgess/redux-most#epicmiddleware) 186 | - [replaceEpic](https://github.com/joshburgess/redux-most#replaceEpic) 187 | - [select](https://github.com/joshburgess/redux-most#select-actiontype-stream) 188 | - [selectArray](https://github.com/joshburgess/redux-most#selectArray-actiontypes-stream) 189 | - [withState](https://github.com/joshburgess/redux-most#withstate-statestream-actionstream) 190 | 191 | --- 192 | 193 | ### `createEpicMiddleware (rootEpic)` 194 | 195 | `createEpicMiddleware` is used to create an instance of the actual `redux-most` middleware. 196 | You provide a single root `Epic`. 197 | 198 | __Arguments__ 199 | 200 | 1. `rootEpic` _(`Epic`)_: The root Epic. 201 | 202 | __Returns__ 203 | 204 | _(`MiddlewareAPI`)_: An instance of the `redux-most` middleware. 205 | 206 | __Example__ 207 | ```js 208 | // redux/configureStore.js 209 | 210 | import { createStore, applyMiddleware, compose } from 'redux' 211 | import { createEpicMiddleware } from 'redux-most' 212 | import { rootEpic, rootReducer } from './modules/root' 213 | 214 | const epicMiddleware = createEpicMiddleware(rootEpic) 215 | 216 | export default function configureStore() { 217 | const store = createStore( 218 | rootReducer, 219 | applyMiddleware(epicMiddleware) 220 | ) 221 | 222 | return store 223 | } 224 | ``` 225 | 226 | --- 227 | 228 | ### `createStateStreamEnhancer (epicMiddleware)` 229 | 230 | `createStateStreamEnhancer` is used to access `redux-most`'s alternate API, which passes 231 | `Epics` a state stream (Ex: `state$`) instead of the `{ dispatch, getState }` store 232 | `MiddlewareAPI` object. You must provide an instance of the `EpicMiddleware`, and the 233 | resulting function must be applied AFTER using `redux`'s `applyMiddleware` if also using 234 | other middleware. 235 | 236 | __Arguments__ 237 | 238 | 1. `rootEpic` _(`Epic`)_: The root Epic. 239 | 240 | __Returns__ 241 | 242 | _(`MiddlewareAPI`)_: An enhanced instance of the `redux-most` middleware, exposing a stream 243 | of state change values. 244 | 245 | __Example__ 246 | ```js 247 | import { createStore, applyMiddleware } from 'redux' 248 | import { 249 | createEpicMiddleware, 250 | createStateStreamEnhancer, 251 | } from 'redux-most' 252 | import rootEpic from '../epics' 253 | 254 | const epicMiddleware = createEpicMiddleware(rootEpic) 255 | const middleware = [...] // other middleware here 256 | const storeEnhancers = compose( 257 | createStateStreamEnhancer(epicMiddleware), 258 | applyMiddleware(...middleware) 259 | ) 260 | 261 | const store = createStore(rootReducer, storeEnhancers) 262 | ``` 263 | 264 | --- 265 | 266 | ### `combineEpics (epicsArray)` 267 | 268 | `combineEpics`, as the name suggests, allows you to pass in an array of epics and combine them into a single one. 269 | 270 | __Arguments__ 271 | 272 | 1. `epicsArray` _(`Epic[]`)_: The array of `epics` to combine into one root epic. 273 | 274 | __Returns__ 275 | 276 | _(`Epic`)_: An Epic that merges the output of every Epic provided and passes along the redux store as arguments. 277 | 278 | __Example__ 279 | ```js 280 | // epics/index.js 281 | 282 | import { combineEpics } from 'redux-most' 283 | import searchUsersDebounced from './searchUsersDebounced' 284 | import searchUsers from './searchUsers' 285 | import clearSearchResults from './clearSearchResults' 286 | import fetchReposByUser from './fetchReposByUser' 287 | import adminAccess from './adminAccess' 288 | 289 | const rootEpic = combineEpics([ 290 | searchUsersDebounced, 291 | searchUsers, 292 | clearSearchResults, 293 | fetchReposByUser, 294 | adminAccess, 295 | ]) 296 | 297 | export default rootEpic 298 | 299 | ``` 300 | 301 | --- 302 | 303 | ### `EpicMiddleware` 304 | 305 | An instance of the `redux-most` middleware. 306 | 307 | To create it, pass your root Epic to [`createEpicMiddleware`](https://github.com/joshburgess/redux-most#createepicmiddleware-rootepic). 308 | 309 | __Methods__ 310 | 311 | - [`replaceEpic (nextEpic)`](https://github.com/joshburgess/redux-most#replaceEpic) 312 | 313 | #### `replaceEpic (nextEpic)` 314 | 315 | Replaces the epic currently used by the middleware. 316 | 317 | It is an advanced API. You might need this if your app implements code splitting and you 318 | want to load some of the epics dynamically or you're using hot reloading. 319 | 320 | __Example__ 321 | 322 | ```js 323 | 324 | import { createEpicMiddleware } from 'redux-most' 325 | import rootEpic from '../epics' 326 | 327 | ... 328 | 329 | const epicMiddleware = createEpicMiddleware(rootEpic) 330 | 331 | ... 332 | 333 | // hot reload epics 334 | const replaceRootEpic = () => { 335 | import('../epics').then( 336 | ({ default: nextRootEpic }) => { epicMiddleware.replaceEpic(nextRootEpic) } 337 | ) 338 | } 339 | 340 | if (module.hot) { 341 | module.hot.accept('../epics', replaceRootEpic) 342 | } 343 | ``` 344 | 345 | __Arguments__ 346 | 347 | 1. `nextEpic` _(`Epic`)_: The next epic for the middleware to use. 348 | 349 | --- 350 | 351 | ### `select (actionType, stream)` 352 | 353 | A helper function for filtering the stream of actions by a single action type. 354 | 355 | __Arguments__ 356 | 357 | 1. `actionType` _(`string`)_: The type of action to filter by. 358 | 2. `stream` _(`Stream`)_: The stream of actions you are filtering. Ex: `actions$`. 359 | 360 | __Returns__ 361 | 362 | _(Stream)_: A new, filtered stream holding only the actions corresponding to the action 363 | type passed to `select`. 364 | 365 | The `select` operator is curried, allowing you to use a fluent or functional style. 366 | 367 | __Examples__ 368 | ```js 369 | // Fluent style 370 | 371 | import { SEARCHED_USERS_DEBOUNCED } from '../constants/ActionTypes' 372 | import { clearSearchResults } from '../actions' 373 | import { select } from 'redux-most' 374 | 375 | const whereEmpty = ({ payload: { query } }) => !query 376 | 377 | const clear = action$ => 378 | action$.thru(select(SEARCHED_USERS_DEBOUNCED)) 379 | .filter(whereEmpty) 380 | .map(clearSearchResults) 381 | 382 | export default clear 383 | ``` 384 | 385 | ```js 386 | // Functional style 387 | 388 | import { SEARCHED_USERS_DEBOUNCED } from '../constants/ActionTypes' 389 | import { clearSearchResults } from '../actions' 390 | import { select } from 'redux-most' 391 | 392 | const whereEmpty = ({ payload: { query } }) => !query 393 | 394 | const clear = action$ => { 395 | const search$ = select(SEARCHED_USERS_DEBOUNCED, action$) 396 | const emptySearch$ = filter(whereEmpty, search$) 397 | return map(clearSearchResults, emptySearch$) 398 | } 399 | 400 | export default clear 401 | ``` 402 | 403 | ```js 404 | // Functional & Pointfree style using functional composition 405 | 406 | import { SEARCHED_USERS_DEBOUNCED } from '../constants/ActionTypes' 407 | import { clearSearchResults } from '../actions' 408 | import { select } from 'redux-most' 409 | import { 410 | curriedFilter as filter, 411 | curriedMap as map, 412 | } from '../utils' 413 | import { compose } from 'ramda' 414 | 415 | const whereEmpty = ({ payload: { query } }) => !query 416 | 417 | const clear = compose( 418 | map(clearSearchResults), 419 | filter(whereEmpty), 420 | select(SEARCHED_USERS_DEBOUNCED) 421 | ) 422 | 423 | export default clear 424 | ``` 425 | --- 426 | 427 | ### `selectArray (actionTypes, stream)` 428 | 429 | A helper function for filtering the stream of actions by an array of action types. 430 | 431 | __Arguments__ 432 | 433 | 1. `actionTypes` _(`string[]`)_: An array of action types to filter by. 434 | 2. `stream` _(`Stream`)_: The stream of actions you are filtering. Ex: `actions$`. 435 | 436 | __Returns__ 437 | 438 | _(Stream)_: A new, filtered stream holding only the actions corresponding to the action 439 | types passed to `selectArray`. 440 | 441 | The `selectArray` operator is curried, allowing you to use a fluent or functional style. 442 | 443 | __Examples__ 444 | ```js 445 | // Fluent style 446 | 447 | import { 448 | SEARCHED_USERS, 449 | SEARCHED_USERS_DEBOUNCED, 450 | } from '../constants/ActionTypes' 451 | import { clearSearchResults } from '../actions' 452 | import { selectArray } from 'redux-most' 453 | 454 | const whereEmpty = ({ payload: { query } }) => !query 455 | 456 | const clear = action$ => 457 | action$.thru(selectArray([ 458 | SEARCHED_USERS, 459 | SEARCHED_USERS_DEBOUNCED, 460 | ])) 461 | .filter(whereEmpty) 462 | .map(clearSearchResults) 463 | 464 | export default clear 465 | ``` 466 | 467 | ```js 468 | // Functional style 469 | 470 | import { 471 | SEARCHED_USERS, 472 | SEARCHED_USERS_DEBOUNCED, 473 | } from '../constants/ActionTypes' 474 | import { clearSearchResults } from '../actions' 475 | import { selectArray } from 'redux-most' 476 | 477 | const whereEmpty = ({ payload: { query } }) => !query 478 | 479 | const clear = action$ => { 480 | const search$ = selectArray([ 481 | SEARCHED_USERS, 482 | SEARCHED_USERS_DEBOUNCED, 483 | ], action$) 484 | const emptySearch$ = filter(whereEmpty, search$) 485 | return map(clearSearchResults, emptySearch$) 486 | } 487 | 488 | export default clear 489 | ``` 490 | 491 | ```js 492 | // Functional & Pointfree style using functional composition 493 | 494 | import { 495 | SEARCHED_USERS, 496 | SEARCHED_USERS_DEBOUNCED, 497 | } from '../constants/ActionTypes' 498 | import { clearSearchResults } from '../actions' 499 | import { selectArray } from 'redux-most' 500 | import { 501 | curriedFilter as filter, 502 | curriedMap as map, 503 | } from '../utils' 504 | import { compose } from 'ramda' 505 | 506 | const whereEmpty = ({ payload: { query } }) => !query 507 | 508 | const clear = compose( 509 | map(clearSearchResults), 510 | filter(whereEmpty), 511 | selectArray([ 512 | SEARCHED_USERS, 513 | SEARCHED_USERS_DEBOUNCED, 514 | ]) 515 | ) 516 | 517 | export default clear 518 | ``` 519 | --- 520 | 521 | ### `withState (stateStream, actionStream)` 522 | 523 | A utility function for use with `redux-most`'s optional state stream API. This 524 | provides a convenient way to `sample` the latest state change value. Note: 525 | accessing the alternate API requires using `createStateStreamEnhancer`. 526 | 527 | __Arguments__ 528 | 529 | 1. `stateStream` _(`Stream`)_: The state stream provided by `redux-most`'s alternate API. 530 | 2. `actionStream` _(`Stream`)_: The filtered stream of action events used to trigger 531 | sampling of the latest state. (Ex: `actions$`). 532 | 533 | __Returns__ 534 | 535 | _(`[state, action]`)_: An Array of length 2 (or Tuple) containing the latest 536 | state value at index 0 and the latest action of the filtered action stream at index 1. 537 | 538 | `withState` is curried, allowing you to pass in the state stream & action stream 539 | together, at the same time, or separately, delaying passing in the action stream. 540 | This provides the user extra flexibility, allowing it to easily be used within 541 | functional composition pipelines. 542 | 543 | __Examples__ 544 | ```js 545 | import { select, withState } from 'redux-most' 546 | import { curriedMap as map } from '../utils' 547 | import compose from 'ramda/src/compose' 548 | 549 | const accessStateFromArray = ([state, action]) => ({ 550 | type: 'ACCESS_STATE', 551 | payload: { 552 | latestState: state, 553 | accessedByAction: action, 554 | }, 555 | }) 556 | 557 | // dispatch { type: 'STATE_STREAM_TEST' } in Redux DevTools to test 558 | const stateStreamTest = (action$, state$) => compose( 559 | map(accessStateFromArray), 560 | withState(state$), 561 | select('STATE_STREAM_TEST') 562 | )(action$) 563 | 564 | export default stateStreamTest 565 | ``` 566 | 567 | --- -------------------------------------------------------------------------------- /examples/navigation-react-redux/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { 4 | "modules": false 5 | }], 6 | "stage-3", 7 | "react" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /examples/navigation-react-redux/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | -------------------------------------------------------------------------------- /examples/navigation-react-redux/README.md: -------------------------------------------------------------------------------- 1 | #### Instructions 2 | ``` 3 | npm install 4 | npm run start 5 | ``` 6 | Then, open your browser and navigate to localhost:3000 7 | -------------------------------------------------------------------------------- /examples/navigation-react-redux/actions/index.js: -------------------------------------------------------------------------------- 1 | import { 2 | ACCESS_DENIED, 3 | CHECKED_ADMIN_ACCESS, 4 | CLEARED_SEARCH_RESULTS, 5 | RECEIVED_USER_REPOS, 6 | RECEIVED_USERS, 7 | REQUESTED_USER_REPOS, 8 | SEARCHED_USERS, 9 | SEARCHED_USERS_DEBOUNCED, 10 | } from '../constants/ActionTypes' 11 | import { curry } from 'ramda' 12 | 13 | 14 | export const searchedUsersDebounced = query => ({ 15 | type: SEARCHED_USERS_DEBOUNCED, 16 | payload: { 17 | query, 18 | }, 19 | }) 20 | 21 | export const searchedUsers = query => ({ 22 | type: SEARCHED_USERS, 23 | payload: { 24 | query, 25 | }, 26 | }) 27 | 28 | export const receiveUsers = users => ({ 29 | type: RECEIVED_USERS, 30 | payload: { 31 | users, 32 | }, 33 | }) 34 | 35 | export const clearSearchResults = _ => ({ 36 | type: CLEARED_SEARCH_RESULTS, 37 | }) 38 | 39 | export const requestReposByUser = user => ({ 40 | type: REQUESTED_USER_REPOS, 41 | payload: { 42 | user, 43 | }, 44 | }) 45 | 46 | export const receiveUserRepos = curry((user, repos) => ({ 47 | type: RECEIVED_USER_REPOS, 48 | payload: { 49 | user, 50 | repos, 51 | }, 52 | })) 53 | 54 | export const checkAdminAccess = _ => ({ 55 | type: CHECKED_ADMIN_ACCESS, 56 | }) 57 | 58 | export const accessDenied = _ => ({ 59 | type: ACCESS_DENIED, 60 | }) 61 | -------------------------------------------------------------------------------- /examples/navigation-react-redux/components/Repos.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const Repos = ({ repos, user }) => 4 | 19 | 20 | export default Repos 21 | -------------------------------------------------------------------------------- /examples/navigation-react-redux/components/UserSearchInput.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const UserSearchInput = ({ value, defaultValue, onChange }) => { 4 | const handleOnChange = evt => onChange(evt.target.value) 5 | 6 | return ( 7 | 13 | ) 14 | } 15 | 16 | export default UserSearchInput 17 | -------------------------------------------------------------------------------- /examples/navigation-react-redux/components/UserSearchResults.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Link } from 'react-router' 3 | 4 | const UserSearchResults = ({ results, loading }) => 5 | 16 | 17 | export default UserSearchResults 18 | -------------------------------------------------------------------------------- /examples/navigation-react-redux/constants/ActionTypes.js: -------------------------------------------------------------------------------- 1 | export const SEARCHED_USERS_DEBOUNCED = 'SEARCHED_USERS_DEBOUNCED' 2 | export const SEARCHED_USERS = 'SEARCHED_USERS' 3 | export const RECEIVED_USERS = 'RECEIVED_USERS' 4 | export const CLEARED_SEARCH_RESULTS = 'CLEARED_SEARCH_RESULTS' 5 | 6 | export const REQUESTED_USER_REPOS = 'REQUESTED_USER_REPOS' 7 | export const RECEIVED_USER_REPOS = 'RECEIVED_USER_REPOS' 8 | 9 | export const CHECKED_ADMIN_ACCESS = 'CHECKED_ADMIN_ACCESS' 10 | export const ACCESS_DENIED = 'ACCESS_DENIED' 11 | -------------------------------------------------------------------------------- /examples/navigation-react-redux/containers/Admin.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { connect } from 'react-redux' 3 | import { checkAdminAccess } from '../actions' 4 | 5 | class Admin extends React.Component { 6 | componentDidMount () { 7 | this.props.checkAdminAccess() 8 | } 9 | 10 | render () { 11 | if (!this.props.adminAccess) { 12 | return ( 13 |

Checking access...

14 | ) 15 | } 16 | 17 | if (this.props.adminAccess === 'GRANTED') { 18 | return ( 19 |

Access granted

20 | ) 21 | } 22 | 23 | return ( 24 |

25 | Access denied. Redirecting back home. 26 |

27 | ) 28 | } 29 | } 30 | 31 | const mapStateToProps = ({ adminAccess }) => ({ adminAccess }) 32 | 33 | const mapDispatchToProps = { checkAdminAccess } 34 | 35 | export default connect(mapStateToProps, mapDispatchToProps)(Admin) 36 | -------------------------------------------------------------------------------- /examples/navigation-react-redux/containers/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const App = ({ children }) => 4 |
5 | {children} 6 |
7 | 8 | export default App 9 | -------------------------------------------------------------------------------- /examples/navigation-react-redux/containers/ReposByUser.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { connect } from 'react-redux' 3 | import Repos from '../components/Repos' 4 | import { requestReposByUser } from '../actions' 5 | 6 | class ReposByUser extends React.Component { 7 | componentDidMount () { 8 | this.props.requestReposByUser(this.props.params.user) 9 | } 10 | 11 | componentWillReceiveProps (nextProps) { 12 | const { user } = this.props.params 13 | 14 | if (user !== nextProps.params.user) { 15 | this.props.requestReposByUser(user) 16 | } 17 | } 18 | 19 | render () { 20 | const { 21 | reposByUser, 22 | user, 23 | } = this.props 24 | 25 | if (!reposByUser[user]) { 26 | return ( 27 |

Loading

28 | ) 29 | } 30 | 31 | return ( 32 | 36 | ) 37 | } 38 | } 39 | 40 | const mapStateToProps = ({ reposByUser }, ownProps) => ({ 41 | reposByUser, 42 | user: ownProps.params.user, 43 | }) 44 | 45 | const mapDispatchToProps = { requestReposByUser } 46 | 47 | export default connect(mapStateToProps, mapDispatchToProps)(ReposByUser) 48 | -------------------------------------------------------------------------------- /examples/navigation-react-redux/containers/Root.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Provider } from 'react-redux' 3 | import { Router, Route, browserHistory, IndexRoute } from 'react-router' 4 | import { syncHistoryWithStore } from 'react-router-redux' 5 | import App from './App' 6 | import UserSearch from './UserSearch' 7 | import ReposByUser from './ReposByUser' 8 | import Admin from './Admin' 9 | 10 | const Root = ({ store }) => 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | export default Root 22 | -------------------------------------------------------------------------------- /examples/navigation-react-redux/containers/UserSearch.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { connect } from 'react-redux' 3 | import { Link } from 'react-router' 4 | import UserSearchInput from '../components/UserSearchInput' 5 | import UserSearchResults from '../components/UserSearchResults' 6 | import { searchedUsersDebounced } from '../actions' 7 | 8 | class UserSearch extends React.Component { 9 | constructor (props) { 10 | super(props) 11 | 12 | this.handleUserSearch = this.handleUserSearch.bind(this) 13 | } 14 | 15 | componentDidMount () { 16 | this.handleUserSearch(this.props.query) 17 | } 18 | 19 | handleUserSearch (query) { 20 | this.props.searchedUsersDebounced(query) 21 | } 22 | 23 | render () { 24 | const { 25 | query, 26 | results, 27 | searchInFlight, 28 | } = this.props 29 | 30 | return ( 31 |
32 | 39 | Admin Panel 40 | 41 | 45 | 49 |
50 | ) 51 | } 52 | } 53 | 54 | const mapStateToProps = ({ routing, userResults, searchInFlight }) => ({ 55 | query: routing.locationBeforeTransitions.query.q, 56 | results: userResults, 57 | searchInFlight, 58 | }) 59 | 60 | const mapDispatchToProps = { searchedUsersDebounced } 61 | 62 | export default connect(mapStateToProps, mapDispatchToProps)(UserSearch) 63 | -------------------------------------------------------------------------------- /examples/navigation-react-redux/epics/adminAccess.js: -------------------------------------------------------------------------------- 1 | import { just } from 'most' 2 | import { 3 | curriedChain as chain, 4 | curriedDelay as delay, 5 | curriedMap as map, 6 | curriedMerge as merge, 7 | } from '../utils' 8 | import { push } from 'react-router-redux' 9 | import { CHECKED_ADMIN_ACCESS } from '../constants/ActionTypes' 10 | import { accessDenied } from '../actions' 11 | import { select } from 'redux-most' 12 | // import { select } from '../../../src/index' 13 | import { compose } from 'ramda' 14 | 15 | const redirectToRoot = _ => push('/') 16 | 17 | // Fluent style 18 | // const adminAccess = action$ => 19 | // action$.thru(select(CHECKED_ADMIN_ACCESS)) 20 | // // If you wanted to do an actual access check you 21 | // // could do so here and then filter by failed checks. 22 | // .delay(800) 23 | // .chain(_ => 24 | // merge( 25 | // just(accessDenied()), 26 | // just().delay(800).map(redirectToRoot) 27 | // ) 28 | // ) 29 | 30 | // Functional style 31 | // const adminAccess = action$ => { 32 | // const checkedAdminAccess$ = select(CHECKED_ADMIN_ACCESS, action$) 33 | // // If you wanted to do an actual access check you 34 | // // could do so here and then filter by failed checks. 35 | // const delayedCheckedAdminAccess$ = delay(800, checkedAdminAccess$) 36 | // const redirectToRoot$ = map(redirectToRoot, delay(800, just())) 37 | // const denyAndRedirect = _ => merge(just(accessDenied()), redirectToRoot$) 38 | // return chain(denyAndRedirect, delayedCheckedAdminAccess$) 39 | // } 40 | 41 | // Functional & Pointfree style using functional composition 42 | const delayedRedirect = compose( 43 | map(redirectToRoot), 44 | delay(800), 45 | just 46 | ) 47 | 48 | const mergeDeniedRedirect = _ => 49 | merge(just(accessDenied()), delayedRedirect()) 50 | 51 | const adminAccess = compose( 52 | chain(mergeDeniedRedirect), 53 | // If you wanted to do an actual access check you 54 | // could do so here and then filter by failed checks. 55 | delay(800), 56 | select(CHECKED_ADMIN_ACCESS) 57 | ) 58 | 59 | export default adminAccess 60 | -------------------------------------------------------------------------------- /examples/navigation-react-redux/epics/clearSearchResults.js: -------------------------------------------------------------------------------- 1 | import { SEARCHED_USERS_DEBOUNCED } from '../constants/ActionTypes' 2 | import { clearSearchResults } from '../actions' 3 | import { select } from 'redux-most' 4 | // import { select } from '../../../src/index' 5 | import { 6 | curriedFilter as filter, 7 | curriedMap as map, 8 | } from '../utils' 9 | import { compose } from 'ramda' 10 | 11 | const whereEmpty = ({ payload: { query } }) => !query 12 | 13 | // Fluent style 14 | // const clear = action$ => 15 | // action$.thru(select(SEARCHED_USERS_DEBOUNCED)) 16 | // .filter(whereEmpty) 17 | // .map(clearSearchResults) 18 | 19 | // Functional style 20 | // const clear = action$ => { 21 | // const search$ = select(SEARCHED_USERS_DEBOUNCED, action$) 22 | // const whereEmpty$ = filter(whereEmpty, search$) 23 | // return map(clearSearchResults, whereEmpty$) 24 | // } 25 | 26 | // Functional & Pointfree style using functional composition 27 | const clear = compose( 28 | map(clearSearchResults), 29 | filter(whereEmpty), 30 | select(SEARCHED_USERS_DEBOUNCED) 31 | ) 32 | 33 | export default clear 34 | -------------------------------------------------------------------------------- /examples/navigation-react-redux/epics/fetchReposByUser.js: -------------------------------------------------------------------------------- 1 | import { REQUESTED_USER_REPOS } from '../constants/ActionTypes' 2 | import { receiveUserRepos } from '../actions' 3 | // import { fromPromise } from 'most' 4 | import { select } from 'redux-most' 5 | // import { select } from '../../../src/index' 6 | import { 7 | curriedMap as map, 8 | curriedSwitchMap as switchMap, 9 | fetchJsonStream, 10 | } from '../utils' 11 | import { compose } from 'ramda' 12 | 13 | const toUser = ({ payload: { user } }) => user 14 | 15 | const getUserReposUrl = user => 16 | `https://api.github.com/users/${user}/repos` 17 | 18 | // Fluent style 19 | // const fetchReposByUser = action$ => 20 | // action$.thru(select(REQUESTED_USER_REPOS)) 21 | // .map(toUser) 22 | // .map(user => 23 | // fromPromise( 24 | // fetch(getUserReposUrl(user)) 25 | // .then(response => response.json()) 26 | // ).map(receiveUserRepos(user)) 27 | // ).switch() 28 | 29 | // NOTE: The below functional implementations use a convenience 30 | // utility called fetchJsonStream. This function is just a 31 | // shortcut for calling fetch, then calling response.json(), & 32 | // then wrapping that resulting promise with Most's fromPromise() 33 | // See utils/index.js for details 34 | 35 | // Functional style 36 | // const fetchAndReceiveUserRepos = user => { 37 | // const repos$ = fetchJsonStream(getUserReposUrl(user)) 38 | // return map(receiveUserRepos(user), repos$) 39 | // } 40 | 41 | // const fetchReposByUser = action$ => { 42 | // const reqUserRepos$ = select(REQUESTED_USER_REPOS, action$) 43 | // const user$ = map(toUser, reqUserRepos$) 44 | // return switchMap(fetchAndReceiveUserRepos, user$) 45 | // } 46 | 47 | // Functional & an almost Pointfree style using functional composition 48 | const fetchAndReceiveUserRepos = user => compose( 49 | map(receiveUserRepos(user)), 50 | fetchJsonStream, 51 | getUserReposUrl 52 | )(user) 53 | 54 | const fetchReposByUser = compose( 55 | switchMap(fetchAndReceiveUserRepos), 56 | map(toUser), 57 | select(REQUESTED_USER_REPOS) 58 | ) 59 | 60 | export default fetchReposByUser 61 | -------------------------------------------------------------------------------- /examples/navigation-react-redux/epics/index.js: -------------------------------------------------------------------------------- 1 | import { combineEpics } from 'redux-most' 2 | // import { combineEpics } from '../../../src/index' 3 | import searchUsersDebounced from './searchUsersDebounced' 4 | import searchUsers from './searchUsers' 5 | import clearSearchResults from './clearSearchResults' 6 | import fetchReposByUser from './fetchReposByUser' 7 | import adminAccess from './adminAccess' 8 | import stateStreamTest from './stateStreamTest' 9 | 10 | const rootEpic = combineEpics([ 11 | searchUsersDebounced, 12 | searchUsers, 13 | clearSearchResults, 14 | fetchReposByUser, 15 | adminAccess, 16 | stateStreamTest, 17 | ]) 18 | 19 | export default rootEpic 20 | -------------------------------------------------------------------------------- /examples/navigation-react-redux/epics/searchUsers.js: -------------------------------------------------------------------------------- 1 | import { replace } from 'react-router-redux' 2 | import { 3 | CLEARED_SEARCH_RESULTS, 4 | SEARCHED_USERS, 5 | } from '../constants/ActionTypes' 6 | import { receiveUsers } from '../actions' 7 | import { 8 | // fromPromise, 9 | just, 10 | } from 'most' 11 | import { 12 | curriedChain as chain, 13 | curriedFilter as filter, 14 | curriedMap as map, 15 | curriedMerge as merge, 16 | curriedSwitchMap as switchMap, 17 | curriedUntil as until, 18 | fetchJsonStream, 19 | } from '../utils' 20 | import { select } from 'redux-most' 21 | // import { select } from '../../../src/index' 22 | import { compose } from 'ramda' 23 | 24 | const toQuery = ({ payload }) => payload.query 25 | 26 | const whereNotEmpty = query => !!query 27 | 28 | const toItems = ({ items }) => items 29 | 30 | const getUsersQueryUrl = query => 31 | `https://api.github.com/search/users?q=${query}` 32 | 33 | const replaceQuery = query => replace(`?q=${query}`) 34 | 35 | // Fluent style 36 | // const searchUsers = action$ => 37 | // action$.thru(select(SEARCHED_USERS)) 38 | // .map(toQuery) 39 | // .filter(whereNotEmpty) 40 | // .map(query => 41 | // just() 42 | // .until(action$.thru(select(CLEARED_SEARCH_RESULTS))) 43 | // .chain(_ => 44 | // merge( 45 | // just(replaceQuery(query)), 46 | // fromPromise( 47 | // fetch(getUsersQueryUrl(query)) 48 | // .then(response => response.json()) 49 | // ) 50 | // .map(toItems) 51 | // .map(receiveUsers) 52 | // ) 53 | // ) 54 | // ).switch() 55 | 56 | // Functional style 57 | // const searchUsers = action$ => { 58 | // const searchedUsers$ = select(SEARCHED_USERS, action$) 59 | // const maybeEmptyQuery$ = map(toQuery, searchedUsers$) 60 | // const query$ = filter(whereNotEmpty, maybeEmptyQuery$) 61 | 62 | // const untilCleared$ = until( 63 | // select(CLEARED_SEARCH_RESULTS, action$), 64 | // just() 65 | // ) 66 | 67 | // const parseJsonForUsers = query => 68 | // map(toItems, fetchJsonStream(getUsersQueryUrl(query))) 69 | 70 | // const fetchReplaceReceive = query => merge( 71 | // just(replaceQuery(query)), 72 | // map(receiveUsers, parseJsonForUsers(query)) 73 | // ) 74 | 75 | // const fetchReplaceReceiveUntilCleared = query => 76 | // chain(_ => fetchReplaceReceive(query), untilCleared$) 77 | 78 | // return switchMap(fetchReplaceReceiveUntilCleared, query$) 79 | // } 80 | 81 | // Using functional composition & simplifying where possible 82 | const searchUsers = action$ => { 83 | const justUntilClearedSearchResults = compose( 84 | until(select(CLEARED_SEARCH_RESULTS, action$)), 85 | just 86 | ) 87 | 88 | const getMergeForQuery = query => 89 | _ => merge( 90 | just(replaceQuery(query)), 91 | compose( 92 | map(compose(receiveUsers, toItems)), 93 | fetchJsonStream, 94 | getUsersQueryUrl 95 | )(query) 96 | ) 97 | 98 | const toFlattenedOutput = query => chain( 99 | getMergeForQuery(query), 100 | justUntilClearedSearchResults() 101 | ) 102 | 103 | return compose( 104 | switchMap(toFlattenedOutput), 105 | filter(whereNotEmpty), 106 | map(toQuery), 107 | select(SEARCHED_USERS) 108 | )(action$) 109 | } 110 | 111 | export default searchUsers 112 | -------------------------------------------------------------------------------- /examples/navigation-react-redux/epics/searchUsersDebounced.js: -------------------------------------------------------------------------------- 1 | import { SEARCHED_USERS_DEBOUNCED } from '../constants/ActionTypes' 2 | import { searchedUsers } from '../actions' 3 | import { select } from 'redux-most' 4 | // import { select } from '../../../src/index' 5 | import { 6 | curriedDebounce as debounce, 7 | curriedMap as map, 8 | } from '../utils' 9 | import { compose } from 'ramda' 10 | 11 | const toSearchedUsers = ({ payload: { query } }) => 12 | searchedUsers(query) 13 | 14 | // Fluent style 15 | // const searchUsersDebounced = action$ => 16 | // action$.thru(select(SEARCHED_USERS_DEBOUNCED)) 17 | // .debounce(800) 18 | // .map(toSearchedUsers) 19 | 20 | // Functional style 21 | // const searchUsersDebounced = action$ => { 22 | // const search$ = select(SEARCHED_USERS_DEBOUNCED, action$) 23 | // const debouncedSearch$ = debounce(800, search$) 24 | // return map(toSearchedUsers, debouncedSearch$) 25 | // } 26 | 27 | // Functional & Pointfree style using functional composition 28 | const searchUsersDebounced = compose( 29 | map(toSearchedUsers), 30 | debounce(800), 31 | select(SEARCHED_USERS_DEBOUNCED) 32 | ) 33 | 34 | export default searchUsersDebounced 35 | -------------------------------------------------------------------------------- /examples/navigation-react-redux/epics/stateStreamTest.js: -------------------------------------------------------------------------------- 1 | import { 2 | select, 3 | withState, 4 | } from 'redux-most' 5 | // import { 6 | // select, 7 | // withState, 8 | // } from '../../../src' 9 | import { 10 | curriedMap as map, 11 | } from '../utils' 12 | import compose from 'ramda/src/compose' 13 | 14 | const accessStateFromArray = ([state, action]) => ({ 15 | type: 'ACCESS_STATE', 16 | payload: { 17 | latestState: state, 18 | accessedByAction: action, 19 | }, 20 | }) 21 | 22 | // dispatch { type: 'STATE_STREAM_TEST' } in Redux DevTools to test 23 | const stateStreamTest = (action$, state$) => compose( 24 | map(accessStateFromArray), 25 | withState(state$), 26 | select('STATE_STREAM_TEST') 27 | )(action$) 28 | 29 | export default stateStreamTest 30 | -------------------------------------------------------------------------------- /examples/navigation-react-redux/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Example navigation app using redux-most 5 | 6 | 7 |
8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /examples/navigation-react-redux/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render } from 'react-dom' 3 | import Root from './containers/Root' 4 | import store from './store' 5 | 6 | // Supply polyfills for new built-ins like Promise, WeakMap, Object.assign, etc. 7 | import 'babel-polyfill' 8 | // Overwrite Promise implementation with Creed for best performance 9 | import { shim } from 'creed' 10 | shim() // eslint-disable-line fp/no-unused-expression 11 | // Supply polyfill for fetch 12 | import 'isomorphic-fetch' 13 | 14 | const rootEl = document.getElementById('app') 15 | 16 | /* eslint-disable fp/no-unused-expression */ 17 | 18 | render(, rootEl) 19 | 20 | /****************************************************************************** 21 | Start development only 22 | *******************************************************************************/ 23 | 24 | const replaceRootComponent = () => { 25 | // import('./containers/root') 26 | // .then( 27 | // ({ default: NextRoot }) => { 28 | // render(, rootEl) 29 | // } 30 | // ) 31 | 32 | const NextRoot = require('./containers/Root').default 33 | render(, rootEl) 34 | } 35 | 36 | if (module.hot) { 37 | module.hot.accept('./containers/Root', replaceRootComponent) 38 | } 39 | 40 | /****************************************************************************** 41 | End development only 42 | *******************************************************************************/ 43 | 44 | /* eslint-enable fp/no-unused-expression */ 45 | -------------------------------------------------------------------------------- /examples/navigation-react-redux/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-most-react-redux-navigation-example", 3 | "version": "0.0.0", 4 | "description": "redux-most react & redux navigation example", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "webpack-dev-server --config webpack.config.dev.babel.js", 8 | "build": "webpack --config webpack.config.prod.babel.js" 9 | }, 10 | "repository": "joshburgess/redux-most", 11 | "author": "Josh Burgess", 12 | "license": "MIT", 13 | "bugs": { 14 | "url": "https://github.com/joshburgess/redux-most/issues" 15 | }, 16 | "homepage": "https://github.com/joshburgess/redux-most#README.md", 17 | "dependencies": { 18 | "babel-polyfill": "^6.23.0", 19 | "creed": "^1.0.3", 20 | "history": "^4.5.1", 21 | "isomorphic-fetch": "^2.2.1", 22 | "most": "^1.0.1", 23 | "most-subject": "^5.3.0", 24 | "ramda": "^0.24.1", 25 | "react": "^15.3.0", 26 | "react-dom": "^15.3.0", 27 | "react-redux": "^5.0.3", 28 | "react-router": "^3.0.2", 29 | "react-router-redux": "^4.0.5", 30 | "redux": "^4.0.0", 31 | "redux-logger": "^3.0.6", 32 | "redux-most": "^0.7.0" 33 | }, 34 | "devDependencies": { 35 | "babel-cli": "^6.26.0", 36 | "babel-core": "^6.26.0", 37 | "babel-eslint": "^7.1.1", 38 | "babel-loader": "^7.1.2", 39 | "babel-preset-env": "^1.6.0", 40 | "babel-preset-react": "^6.24.1", 41 | "babel-preset-stage-3": "^6.22.0", 42 | "babel-register": "^6.26.0", 43 | "webpack": "^3.5.5", 44 | "webpack-dev-server": "^2.4.5" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /examples/navigation-react-redux/reducers/adminAccess.js: -------------------------------------------------------------------------------- 1 | import { ACCESS_DENIED } from '../constants/ActionTypes' 2 | 3 | const DENIED = 'DENIED' 4 | 5 | const adminAccess = (state = null, action) => { 6 | switch (action.type) { 7 | case ACCESS_DENIED: 8 | return DENIED 9 | default: 10 | return state 11 | } 12 | } 13 | 14 | export default adminAccess 15 | -------------------------------------------------------------------------------- /examples/navigation-react-redux/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux' 2 | import { routerReducer } from 'react-router-redux' 3 | import userResults from './userResults' 4 | import searchInFlight from './searchInFlight' 5 | import reposByUser from './reposByUser' 6 | import adminAccess from './adminAccess' 7 | 8 | const rootReducer = combineReducers({ 9 | userResults, 10 | searchInFlight, 11 | reposByUser, 12 | adminAccess, 13 | routing: routerReducer, 14 | }) 15 | 16 | export default rootReducer 17 | -------------------------------------------------------------------------------- /examples/navigation-react-redux/reducers/reposByUser.js: -------------------------------------------------------------------------------- 1 | import { 2 | RECEIVED_USER_REPOS, 3 | REQUESTED_USER_REPOS, 4 | } from '../constants/ActionTypes' 5 | 6 | const reposByUser = (state = {}, action) => { 7 | switch (action.type) { 8 | case REQUESTED_USER_REPOS: 9 | return Object.assign({}, state, { 10 | [action.payload.user]: undefined, 11 | }) 12 | case RECEIVED_USER_REPOS: 13 | return Object.assign({}, state, { 14 | [action.payload.user]: action.payload.repos, 15 | }) 16 | default: 17 | return state 18 | } 19 | } 20 | 21 | export default reposByUser 22 | -------------------------------------------------------------------------------- /examples/navigation-react-redux/reducers/searchInFlight.js: -------------------------------------------------------------------------------- 1 | import { 2 | CLEARED_SEARCH_RESULTS, 3 | RECEIVED_USERS, 4 | SEARCHED_USERS_DEBOUNCED, 5 | } from '../constants/ActionTypes' 6 | 7 | const searchInFlight = (state = false, action) => { 8 | switch (action.type) { 9 | case SEARCHED_USERS_DEBOUNCED: 10 | return true 11 | case RECEIVED_USERS: 12 | case CLEARED_SEARCH_RESULTS: 13 | return false 14 | default: 15 | return state 16 | } 17 | } 18 | 19 | export default searchInFlight 20 | -------------------------------------------------------------------------------- /examples/navigation-react-redux/reducers/userResults.js: -------------------------------------------------------------------------------- 1 | import { 2 | CLEARED_SEARCH_RESULTS, 3 | RECEIVED_USERS, 4 | } from '../constants/ActionTypes' 5 | 6 | const initialState = [] 7 | 8 | const userResults = (state = initialState, action) => { 9 | switch (action.type) { 10 | case RECEIVED_USERS: 11 | return action.payload.users 12 | case CLEARED_SEARCH_RESULTS: 13 | return initialState 14 | default: 15 | return state 16 | } 17 | } 18 | 19 | export default userResults 20 | -------------------------------------------------------------------------------- /examples/navigation-react-redux/store/index.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware } from 'redux' 2 | // Use Ramda's compose instead of Redux's compose, 3 | // because we're already using it elsewhere. 4 | import compose from 'ramda/src/compose' 5 | import { 6 | createEpicMiddleware, 7 | createStateStreamEnhancer, 8 | } from 'redux-most' 9 | // import { 10 | // createEpicMiddleware, 11 | // createStateStreamEnhancer, 12 | // } from '../../../src' 13 | import { createLogger } from 'redux-logger' 14 | import { browserHistory } from 'react-router' 15 | import { routerMiddleware } from 'react-router-redux' 16 | import rootReducer from '../reducers' 17 | import rootEpic from '../epics' 18 | 19 | const epicMiddleware = createEpicMiddleware(rootEpic) 20 | 21 | const logger = createLogger({ 22 | collapsed: true, 23 | diff: false, 24 | logErrors: true, 25 | }) 26 | 27 | const middleware = [ 28 | logger, 29 | // epicMiddleware, 30 | routerMiddleware(browserHistory), 31 | ] 32 | 33 | const composeEnhancers = 34 | typeof window === 'object' && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ 35 | ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({ 36 | // options here 37 | }) 38 | : compose 39 | 40 | const storeEnhancers = composeEnhancers( 41 | createStateStreamEnhancer(epicMiddleware), 42 | applyMiddleware(...middleware) 43 | ) 44 | 45 | const store = createStore(rootReducer, storeEnhancers) 46 | 47 | /****************************************************************************** 48 | Start development only 49 | *******************************************************************************/ 50 | 51 | // hot reload epics 52 | const replaceRootEpic = () => { 53 | // import('../epics').then( 54 | // ({ default: nextRootEpic }) => { epicMiddleware.replaceEpic(nextRootEpic) } 55 | // ) 56 | 57 | const nextRootEpic = require('../epics').default 58 | epicMiddleware.replaceEpic(nextRootEpic) 59 | } 60 | 61 | if (module.hot) { 62 | module.hot.accept('../epics', replaceRootEpic) 63 | } 64 | 65 | // hot reload reducers 66 | const replaceRootReducer = () => { 67 | // import('../reducers').then( 68 | // ({ default: nextRootReducer }) => { 69 | // store.replaceReducer(nextRootReducer) 70 | // } 71 | // ) 72 | 73 | const nextRootReducer = require('../reducers').default 74 | store.replaceReducer(nextRootReducer) 75 | } 76 | 77 | if (module.hot) { 78 | module.hot.accept('../reducers', replaceRootReducer) 79 | } 80 | 81 | /****************************************************************************** 82 | End development only 83 | *******************************************************************************/ 84 | 85 | export default store 86 | -------------------------------------------------------------------------------- /examples/navigation-react-redux/utils/index.js: -------------------------------------------------------------------------------- 1 | import { compose, curry } from 'ramda' 2 | import { 3 | chain, 4 | debounce, 5 | delay, 6 | filter, 7 | fromPromise, 8 | map, 9 | merge, 10 | switchLatest, 11 | tap, 12 | throttle, 13 | until, 14 | } from 'most' 15 | 16 | /****************************************************************************** 17 | General utilities 18 | *******************************************************************************/ 19 | 20 | // used to persist native events instead of using React's synthetic events 21 | export const nativeEventPersist = f => syntheticEvent => 22 | syntheticEvent.persist() || f(syntheticEvent.nativeEvent) 23 | 24 | export const isString = str => str && 25 | (typeof str === 'string' || str instanceof String) 26 | 27 | export const noOp = f => f 28 | 29 | export const log = console.log 30 | export const tapLog = x => log(x) || x 31 | export const tapLogL = curry((label, x) => log(label, x) || x) 32 | 33 | export const then = curry((f, thenable) => thenable.then(f)) 34 | export const toJson = response => response.json() 35 | export const fetchJson = compose(then(toJson), fetch) 36 | export const fetchJsonStream = compose(fromPromise, fetchJson) 37 | 38 | /****************************************************************************** 39 | Stream utilities 40 | *******************************************************************************/ 41 | 42 | const mapTo = (x, stream) => map(_ => x, stream) 43 | const switchMap = compose(switchLatest, map) 44 | 45 | // prefix with "curried" so things are more obvious in other files 46 | export const curriedChain = curry(chain) 47 | export const curriedDebounce = curry(debounce) 48 | export const curriedDelay = curry(delay) 49 | export const curriedFilter = curry(filter) 50 | export const curriedMap = curry(map) 51 | export const curriedMapTo = curry(mapTo) 52 | export const curriedMerge = curry(merge) 53 | export const curriedSwitchMap = curry(switchMap) 54 | export const curriedTap = curry(tap) 55 | export const curriedThrottle = curry(throttle) 56 | export const curriedUntil = curry(until) 57 | -------------------------------------------------------------------------------- /examples/navigation-react-redux/webpack.config.dev.babel.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-commonjs */ 2 | 3 | const path = require('path') 4 | const webpack = require('webpack') 5 | 6 | const PATH_SRC = path.join(__dirname) 7 | const PATH_DIST = path.join(__dirname, 'dist') 8 | const PATH_PUBLIC = '/static/' 9 | 10 | const config = { 11 | entry: PATH_SRC, 12 | output: { 13 | path: PATH_DIST, 14 | filename: 'bundle.js', 15 | publicPath: PATH_PUBLIC, 16 | }, 17 | devServer: { 18 | contentBase: PATH_SRC, 19 | historyApiFallback: { 20 | index: PATH_PUBLIC, 21 | }, 22 | hot: true, 23 | inline: true, 24 | open: true, 25 | port: 3000, 26 | }, 27 | plugins: [ 28 | new webpack.DefinePlugin({ 29 | process: { 30 | env: { 31 | NODE_ENV: JSON.stringify('development'), 32 | }, 33 | }, 34 | }), 35 | new webpack.HotModuleReplacementPlugin(), 36 | new webpack.NoEmitOnErrorsPlugin(), 37 | ], 38 | module: { 39 | rules: [ 40 | { 41 | test: /\.js$/, 42 | loader: 'babel-loader', 43 | exclude: /node_modules/, 44 | query: { 45 | 'presets': [ 46 | ['env', { 47 | 'modules': false, 48 | }], 49 | 'stage-3', 50 | 'react', 51 | ], 52 | }, 53 | }, 54 | ], 55 | }, 56 | resolve: { 57 | extensions: ['.js', '.jsx'], 58 | }, 59 | } 60 | 61 | module.exports = config 62 | -------------------------------------------------------------------------------- /examples/navigation-react-redux/webpack.config.prod.babel.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-commonjs */ 2 | 3 | const path = require('path') 4 | const webpack = require('webpack') 5 | 6 | const PATH_SRC = path.join(__dirname) 7 | const PATH_DIST = path.join(__dirname, 'dist') 8 | const PATH_PUBLIC = '/static/' 9 | 10 | const config = { 11 | entry: PATH_SRC, 12 | output: { 13 | path: PATH_DIST, 14 | filename: 'bundle.js', 15 | publicPath: PATH_PUBLIC, 16 | }, 17 | plugins: [ 18 | new webpack.DefinePlugin({ 19 | process: { 20 | env: { 21 | NODE_ENV: JSON.stringify('production'), 22 | }, 23 | }, 24 | }), 25 | new webpack.optimize.UglifyJsPlugin({ 26 | compressor: { 27 | screw_ie8: true, 28 | warnings: false, 29 | }, 30 | }), 31 | ], 32 | module: { 33 | rules: [ 34 | { 35 | test: /\.js$/, 36 | loader: 'babel-loader', 37 | exclude: /node_modules/, 38 | query: { 39 | 'presets': [ 40 | ['env', { 41 | 'modules': false, 42 | }], 43 | 'stage-3', 44 | 'react', 45 | ], 46 | }, 47 | }, 48 | ], 49 | }, 50 | resolve: { 51 | extensions: ['.js', '.jsx'], 52 | }, 53 | } 54 | 55 | module.exports = config 56 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Action, 3 | Dispatch, 4 | Middleware, 5 | MiddlewareAPI, 6 | StoreEnhancer, 7 | } from 'redux' 8 | import { Stream } from 'most' 9 | 10 | /***************************************** 11 | Type abbreviations: 12 | A = Action 13 | T = ActionType (a string or symbol) 14 | S = State 15 | *****************************************/ 16 | 17 | // default to the most common use case, but allow overriding 18 | export type ActionType = string 19 | 20 | export interface DefaultAction extends Action { 21 | [key: string]: any 22 | } 23 | 24 | // for the original, redux-observable style API 25 | export type OriginalApiEpic = ( 26 | actionStream: Stream, 27 | middlewareApi: MiddlewareAPI, S>, 28 | ) => Stream 29 | 30 | // for the newer, declarative only API, which takes in a state stream 31 | // to sample via the withState utility instead of exposing dispatch/getState 32 | export type DeclarativeApiEpic = ( 33 | actionStream: Stream, 34 | stateStream: Stream, 35 | ) => Stream 36 | 37 | export type Epic = 38 | | OriginalApiEpic 39 | | DeclarativeApiEpic 40 | 41 | export interface EpicMiddleware 42 | extends Middleware { 43 | replaceEpic(nextEpic: Epic): void 44 | } 45 | 46 | export declare function createEpicMiddleware< 47 | S, 48 | A extends Action = DefaultAction 49 | >(rootEpic: Epic): EpicMiddleware 50 | 51 | export declare function createStateStreamEnhancer< 52 | S, 53 | A extends Action = DefaultAction 54 | >(epicMiddleware: EpicMiddleware): StoreEnhancer 55 | 56 | export declare function combineEpics( 57 | epicsArray: Epic[], 58 | ): Epic 59 | 60 | // overloads exist due to select being a curried function 61 | export declare function select< 62 | A extends Action = DefaultAction, 63 | T = ActionType 64 | >(actionType: T, stream: Stream): Stream 65 | 66 | export declare function select< 67 | A extends Action = DefaultAction, 68 | T = ActionType 69 | >(actionType: T): (stream: Stream) => Stream 70 | 71 | // overloads exist due to selectArray being a curried function 72 | export declare function selectArray< 73 | A extends Action = DefaultAction, 74 | T = ActionType 75 | >(actionTypes: T[], stream: Stream): Stream 76 | 77 | export declare function selectArray< 78 | A extends Action = DefaultAction, 79 | T = ActionType 80 | >(actionTypes: T[]): (stream: Stream) => Stream 81 | 82 | // overloads exist due to withState being a curried function 83 | export declare function withState( 84 | stateStream: Stream, 85 | actionStream: Stream, 86 | ): Stream<[S, A]> 87 | 88 | export declare function withState( 89 | stateStream: Stream, 90 | ): (actionStream: Stream) => Stream<[S, A]> 91 | 92 | export const EPIC_END = '@@redux-most/EPIC_END' 93 | export type EPIC_END = typeof EPIC_END 94 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-most", 3 | "version": "0.8.0", 4 | "description": "Most.js based middleware for Redux. Handle async actions with monadic streams and reactive programming.", 5 | "main": "lib/index.js", 6 | "module": "es/index.js", 7 | "jsnext:main": "es/index.js", 8 | "typings": "./index.d.ts", 9 | "files": [ 10 | "dist", 11 | "lib", 12 | "es", 13 | "src", 14 | "index.d.ts" 15 | ], 16 | "scripts": { 17 | "lint": "eslint src", 18 | "test": "cross-env BABEL_ENV=test ava --tap | tap-diff", 19 | "tdd": "cross-env BABEL_ENV=test ava --watch", 20 | "safety-check": "yarn lint && yarn test", 21 | "build:cjs": "rimraf lib && cross-env BABEL_ENV=cjs babel src --out-dir lib", 22 | "build:es": "rimraf es && cross-env BABEL_ENV=es babel src --out-dir es", 23 | "build:umd": "rimraf dist && cross-env BABEL_ENV=umd webpack --config webpack.config.babel.js", 24 | "build": "yarn build:cjs && yarn build:es && yarn build:umd", 25 | "prepublish": "yarn safety-check && yarn build" 26 | }, 27 | "repository": "joshburgess/redux-most", 28 | "keywords": [ 29 | "action", 30 | "async", 31 | "asynchronous", 32 | "fluent", 33 | "functional", 34 | "middleware", 35 | "monad", 36 | "monadic", 37 | "most", 38 | "most.js", 39 | "mostjs", 40 | "observable", 41 | "reactive", 42 | "reactive extensions", 43 | "reactive programming", 44 | "reactive streams", 45 | "redux", 46 | "redux-observable", 47 | "redux-saga", 48 | "rx", 49 | "rxjs", 50 | "saga", 51 | "sagas", 52 | "stream", 53 | "streams", 54 | "thunk" 55 | ], 56 | "author": { 57 | "name": "Josh Burgess", 58 | "email": "joshburgess.webdev@gmail.com" 59 | }, 60 | "license": "MIT", 61 | "bugs": { 62 | "url": "https://github.com/joshburgess/redux-most/issues" 63 | }, 64 | "homepage": "https://github.com/joshburgess/redux-most#README.md", 65 | "ava": { 66 | "files": [ 67 | "tests/*.test.js" 68 | ], 69 | "failFast": true, 70 | "require": [ 71 | "babel-register" 72 | ], 73 | "babel": "inherit" 74 | }, 75 | "peerDependencies": { 76 | "most": "1.*", 77 | "redux": "^4.0.1" 78 | }, 79 | "dependencies": { 80 | "most-subject": "^5.3.0" 81 | }, 82 | "devDependencies": { 83 | "ava": "^0.22.0", 84 | "babel-cli": "^6.11.4", 85 | "babel-eslint": "^7.1.1", 86 | "babel-loader": "^7.0.0", 87 | "babel-preset-env": "^1.5.0", 88 | "babel-preset-react": "^6.24.1", 89 | "babel-preset-stage-3": "^6.22.0", 90 | "babel-register": "^6.24.1", 91 | "cross-env": "^5.0.0", 92 | "eslint": "^4.6.1", 93 | "eslint-config-standard-pure-fp": "^2.0.1", 94 | "eslint-config-standard-react": "^5.0.0", 95 | "eslint-plugin-better": "0.1.5", 96 | "eslint-plugin-fp": "^2.3.0", 97 | "eslint-plugin-import": "^2.2.0", 98 | "eslint-plugin-promise": "^3.5.0", 99 | "eslint-plugin-react": "^7.0.1", 100 | "eslint-plugin-standard": "^3.0.1", 101 | "nyc": "^11.1.0", 102 | "rimraf": "^2.5.4", 103 | "sinon": "^3.2.1", 104 | "tap-diff": "^0.1.1", 105 | "typescript": "^3.2.2", 106 | "typings": "^2.1.0", 107 | "webpack": "^3.5.5" 108 | }, 109 | "npmName": "redux-most", 110 | "npmFileMap": [ 111 | { 112 | "basePath": "/dist/", 113 | "files": [ 114 | "*.js" 115 | ] 116 | } 117 | ] 118 | } 119 | -------------------------------------------------------------------------------- /src/actions.js: -------------------------------------------------------------------------------- 1 | import { EPIC_END } from './constants' 2 | 3 | export const epicEnd = () => ({ type: EPIC_END }) 4 | -------------------------------------------------------------------------------- /src/combineEpics.js: -------------------------------------------------------------------------------- 1 | import { mergeArray } from 'most' 2 | import { findIndex, map } from '@most/prelude' 3 | 4 | export const combineEpics = epicsArray => ( 5 | actionsStream, 6 | middlewareApiOrStateStream, 7 | ) => { 8 | if (!epicsArray || !Array.isArray(epicsArray)) { 9 | throw new TypeError('You must provide an array of Epics to combineEpics.') 10 | } 11 | 12 | if (epicsArray.length < 1) { 13 | throw new TypeError( 14 | 'The array passed to combineEpics must contain at least one Epic.', 15 | ) 16 | } 17 | 18 | const callEpic = epic => { 19 | if (typeof epic !== 'function') { 20 | throw new TypeError( 21 | 'The array passed to combineEpics must contain only Epics (functions).', 22 | ) 23 | } 24 | 25 | const out = epic(actionsStream, middlewareApiOrStateStream) 26 | 27 | if (!out || !out.source) { 28 | const epicIdentifier = epic.name 29 | ? `named ${epic.name}` 30 | : `at index ${findIndex(epic, epicsArray)} of the passed in array` 31 | 32 | throw new TypeError( 33 | `All Epics in the array provided to combineEpics must return a stream. Check the return value of the Epic ${epicIdentifier}.`, 34 | ) 35 | } 36 | 37 | return out 38 | } 39 | 40 | return mergeArray(map(callEpic, epicsArray)) 41 | } 42 | -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | export const EPIC_END = '@@redux-most/EPIC_END' 2 | export const STATE_STREAM_SYMBOL = Symbol('@@redux-most/STATE_STREAM') 3 | -------------------------------------------------------------------------------- /src/createEpicMiddleware.js: -------------------------------------------------------------------------------- 1 | import { map, observe, switchLatest } from 'most' 2 | import { sync } from 'most-subject' 3 | import { epicEnd } from './actions' 4 | import { STATE_STREAM_SYMBOL } from './constants' 5 | 6 | export const createEpicMiddleware = epic => { 7 | if (typeof epic !== 'function') { 8 | throw new TypeError('You must provide an Epic (a function) to createEpicMiddleware.') 9 | } 10 | 11 | // it is important that this stream is created here and passed in to each 12 | // epic so that all epics act on the same action$, because this is what 13 | // allows debouncing, throttling, etc. to work correctly on subsequent 14 | // dispatched actions of the same type 15 | const actionsIn$ = sync() 16 | 17 | // epic$ must be a Subject, because replaceEpic cannot be written without it 18 | const epic$ = sync() 19 | 20 | // middlewareApi is mutable and defined here in order to capture a reference to the 21 | // _middlewareApi argument so that dispatch can be called from within replaceEpic 22 | let middlewareApi // eslint-disable-line fp/no-let 23 | 24 | const epicMiddleware = _middlewareApi => { 25 | middlewareApi = _middlewareApi 26 | 27 | return next => { 28 | const callNextEpic = nextEpic => { 29 | const state$ = middlewareApi[STATE_STREAM_SYMBOL] 30 | const isUsingStateStreamEnhancer = !!state$ 31 | 32 | return isUsingStateStreamEnhancer 33 | // new style API (declarative only, no dispatch/getState) 34 | ? nextEpic(actionsIn$, state$) 35 | // redux-observable style Epic API 36 | : nextEpic(actionsIn$, middlewareApi) 37 | } 38 | 39 | const actionsOut$ = switchLatest(map(callNextEpic, epic$)) 40 | observe(middlewareApi.dispatch, actionsOut$) 41 | 42 | // Emit combined epics 43 | epic$.next(epic) 44 | 45 | return action => { 46 | // Allow reducers to receive actions before epics 47 | const result = next(action) 48 | actionsIn$.next(action) 49 | return result 50 | } 51 | } 52 | } 53 | 54 | // can be used for hot reloading, code splitting, etc. 55 | epicMiddleware.replaceEpic = nextEpic => { 56 | middlewareApi.dispatch(epicEnd()) 57 | epic$.next(nextEpic) 58 | } 59 | 60 | return epicMiddleware 61 | } 62 | -------------------------------------------------------------------------------- /src/createStateStreamEnhancer.js: -------------------------------------------------------------------------------- 1 | import { from, skipRepeats } from 'most' 2 | import { STATE_STREAM_SYMBOL } from './constants' 3 | 4 | export const createStateStreamEnhancer = epicMiddleware => createStore => 5 | (reducer, preloadedState, enhancer) => { 6 | const store = createStore(reducer, preloadedState, enhancer) 7 | let dispatch = store.dispatch // eslint-disable-line fp/no-let 8 | 9 | const middlewareApi = { 10 | getState: store.getState, 11 | dispatch: action => dispatch(action), 12 | [STATE_STREAM_SYMBOL]: skipRepeats(from(store)), 13 | } 14 | 15 | dispatch = epicMiddleware(middlewareApi)(store.dispatch) 16 | 17 | return { 18 | ...store, 19 | dispatch, 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export { combineEpics } from './combineEpics' 2 | export { createEpicMiddleware } from './createEpicMiddleware' 3 | export { createStateStreamEnhancer } from './createStateStreamEnhancer' 4 | export { EPIC_END } from './constants' 5 | export { select } from './select' 6 | export { selectArray } from './selectArray' 7 | export { withState } from './withState' 8 | -------------------------------------------------------------------------------- /src/select.js: -------------------------------------------------------------------------------- 1 | import { filter } from 'most' 2 | import { curry2 } from '@most/prelude' 3 | 4 | export const select = curry2((actionType, stream) => 5 | filter(({ type }) => type && type === actionType, stream)) 6 | -------------------------------------------------------------------------------- /src/selectArray.js: -------------------------------------------------------------------------------- 1 | import { filter } from 'most' 2 | import { curry2, findIndex } from '@most/prelude' 3 | 4 | export const selectArray = curry2((actionTypes, stream) => 5 | filter(({ type }) => type && findIndex(type, actionTypes) !== -1, stream)) 6 | -------------------------------------------------------------------------------- /src/withState.js: -------------------------------------------------------------------------------- 1 | import { sampleArray } from 'most' 2 | import { curry3 } from '@most/prelude' 3 | 4 | const flippedSampleState = curry3((f, stateStream, samplerStream) => 5 | sampleArray(f, samplerStream, [stateStream, samplerStream])) 6 | 7 | const toArray = (state, samplerStreamEvent) => [state, samplerStreamEvent] 8 | 9 | export const withState = flippedSampleState(toArray) 10 | -------------------------------------------------------------------------------- /tests/combineEpics.test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import { map, observe } from 'most' 3 | import { sync } from 'most-subject' 4 | import { combineEpics, select } from '../src/' 5 | 6 | test('combineEpics should combine an array of epics', t => { 7 | const ACTION_1 = 'ACTION_1' 8 | const ACTION_2 = 'ACTION_2' 9 | const DELEGATED_1 = 'DELEGATED_1' 10 | const DELEGATED_2 = 'DELEGATED_2' 11 | const MOCKED_STORE = { I: 'am', a: 'store' } 12 | 13 | const epic1 = (actions$, store) => map( 14 | action => ({ type: DELEGATED_1, action, store }), 15 | select(ACTION_1, actions$) 16 | ) 17 | 18 | const epic2 = (actions$, store) => map( 19 | action => ({ type: DELEGATED_2, action, store }), 20 | select(ACTION_2, actions$) 21 | ) 22 | 23 | const epic = combineEpics([ 24 | epic1, 25 | epic2, 26 | ]) 27 | 28 | const store = MOCKED_STORE 29 | const actions$ = sync() 30 | const result$ = epic(actions$, store) 31 | const emittedActions = [] 32 | 33 | observe(emittedAction => emittedActions.push(emittedAction), result$) 34 | 35 | actions$.next({ type: ACTION_1 }) 36 | actions$.next({ type: ACTION_2 }) 37 | 38 | const MOCKED_EMITTED_ACTIONS = [ 39 | { type: DELEGATED_1, action: { type: ACTION_1 }, store }, 40 | { type: DELEGATED_2, action: { type: ACTION_2 }, store }, 41 | ] 42 | 43 | t.deepEqual(MOCKED_EMITTED_ACTIONS, emittedActions) 44 | }) 45 | -------------------------------------------------------------------------------- /tests/select.test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import { observe } from 'most' 3 | import { sync } from 'most-subject' 4 | import { select } from '../src/' 5 | 6 | test('select should filter by action type', t => { 7 | const actions$ = sync() 8 | const lulz = [] 9 | const haha = [] 10 | 11 | observe(x => lulz.push(x), select('LULZ', actions$)) 12 | observe(x => haha.push(x), select('HAHA', actions$)) 13 | 14 | actions$.next({ type: 'LULZ', i: 0 }) 15 | 16 | t.deepEqual([{ type: 'LULZ', i: 0 }], lulz) 17 | t.deepEqual([], haha) 18 | 19 | actions$.next({ type: 'LULZ', i: 1 }) 20 | 21 | t.deepEqual([{ type: 'LULZ', i: 0 }, { type: 'LULZ', i: 1 }], lulz) 22 | t.deepEqual([], haha) 23 | 24 | actions$.next({ type: 'HAHA', i: 0 }) 25 | 26 | t.deepEqual([{ type: 'LULZ', i: 0 }, { type: 'LULZ', i: 1 }], lulz) 27 | t.deepEqual([{ type: 'HAHA', i: 0 }], haha) 28 | }) 29 | -------------------------------------------------------------------------------- /tests/selectArray.test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import { observe } from 'most' 3 | import { sync } from 'most-subject' 4 | import { selectArray } from '../src/' 5 | 6 | test('selectArray should filter by multiple action types', t => { 7 | const actions$ = sync() 8 | const lulz = [] 9 | const haha = [] 10 | 11 | observe(x => lulz.push(x), selectArray(['LULZ', 'LMFAO'], actions$)) 12 | observe(x => haha.push(x), selectArray(['HAHA'], actions$)) 13 | 14 | actions$.next({ type: 'LULZ', i: 0 }) 15 | 16 | t.deepEqual([{ type: 'LULZ', i: 0 }], lulz) 17 | t.deepEqual([], haha) 18 | 19 | actions$.next({ type: 'LMFAO', i: 1 }) 20 | 21 | t.deepEqual([{ type: 'LULZ', i: 0 }, { type: 'LMFAO', i: 1 }], lulz) 22 | t.deepEqual([], haha) 23 | 24 | actions$.next({ type: 'HAHA', i: 0 }) 25 | 26 | t.deepEqual([{ type: 'LULZ', i: 0 }, { type: 'LMFAO', i: 1 }], lulz) 27 | t.deepEqual([{ type: 'HAHA', i: 0 }], haha) 28 | }) 29 | -------------------------------------------------------------------------------- /webpack.config.babel.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-commonjs */ 2 | 3 | const webpack = require('webpack') 4 | 5 | /****************************************************************************** 6 | Base config (used in both development & production) 7 | *******************************************************************************/ 8 | 9 | const baseConfig = { 10 | entry: './src/index.js', 11 | module: { 12 | rules: [ 13 | { 14 | test: /\.js$/, 15 | use: 'babel-loader', 16 | exclude: /node_modules/, 17 | }, 18 | ], 19 | }, 20 | output: { 21 | library: 'ReduxMost', 22 | libraryTarget: 'umd', 23 | }, 24 | externals: { 25 | most: { 26 | root: 'most', 27 | commonjs2: 'most', 28 | commonjs: 'most', 29 | amd: 'most', 30 | }, 31 | redux: { 32 | root: 'Redux', 33 | commonjs2: 'redux', 34 | commonjs: 'redux', 35 | amd: 'redux', 36 | }, 37 | }, 38 | resolve: { 39 | extensions: ['.js'], 40 | mainFields: ['module', 'main', 'jsnext:main'], 41 | }, 42 | } 43 | 44 | /****************************************************************************** 45 | Development config (Unminified redux-most.js UMD build) 46 | *******************************************************************************/ 47 | 48 | const devConfig = { 49 | ...baseConfig, 50 | output: { 51 | ...baseConfig.output, 52 | filename: './dist/redux-most.js', 53 | }, 54 | plugins: [ 55 | new webpack.EnvironmentPlugin({ 56 | 'NODE_ENV': 'development', 57 | }), 58 | ], 59 | } 60 | 61 | /****************************************************************************** 62 | Production config (Minified redux-most.min.js UMD build) 63 | *******************************************************************************/ 64 | 65 | const prodConfig = { 66 | ...baseConfig, 67 | output: { 68 | ...baseConfig.output, 69 | filename: './dist/redux-most.min.js', 70 | }, 71 | plugins: [ 72 | new webpack.EnvironmentPlugin({ 73 | 'NODE_ENV': 'production', 74 | }), 75 | new webpack.optimize.UglifyJsPlugin({ 76 | compress: { 77 | screw_ie8: true, 78 | warnings: false, 79 | }, 80 | comments: false, 81 | }), 82 | ], 83 | } 84 | 85 | // This is a not well documented feature of Webpack. When exporting an array 86 | // Webpack 2 will run multiple times, once for each config in the array 87 | // (synchronously, from first to last). This is what allows us to use multiple 88 | // configs in a single file. 89 | module.exports = [devConfig, prodConfig] 90 | --------------------------------------------------------------------------------