├── .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 |
{user} has no repos
17 | } 18 |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 |Loading
28 | ) 29 | } 30 | 31 | return ( 32 |