Code Monkey home page Code Monkey logo

redux-most's Introduction

redux-most

Most.js based middleware for Redux.

Handle async actions with monadic streams & reactive programming.

Install

With yarn (recommended):

yarn add redux-most

or with npm:

npm install --save redux-most

Additionally, make sure the peer dependencies, redux and most, are also installed.

Background

redux-most is based on redux-observable. It uses the same pattern/concept of "epics" without requiring RxJS 5 as a peer dependency. Although redux-observable does provide capability for using other stream libraries via adapters, redux-most allows you to bypass needing to install both RxJS 5 and Most. I prefer Most for working with observables and would rather have minimal dependencies. So, I wrote this middleware primarily for my own use.

Please, see redux-observable's documentation for details on usage.

Why Most over RxJS?

RxJS 5 is great. It's quite a bit faster than RxJS 4, and Rx, in general, is a very useful tool which happens to exist across many different languages. Learning it is definitely a good idea. However, Most is significantly smaller, less complicated, and faster than RxJS 5. I prefer its more minimal set of operators and its focus on performance. Also, like Ramda or lodash/fp, Most supports a functional API in which the data collection (a stream, rather than an array, in this case) gets passed in last. This is important, because it allows you to use functional programming techniques like currying & partial application, which you can't do with RxJS without writing your own wrapper functions, because it only offers an OOP/fluent/chaining style API.

Why integrate Most/RxJS with redux instead of recreating it with streams?

It's true that it's quite easy to implement the core ideas of Redux with observables using the scan operator. (See my inferno-most-fp-demo for an example.) However, the Redux DevTools provide what is arguably the nicest developer tooling experience currently available in the JavaScript ecosystem. Therefore, it is huge to be able to maintain it as an asset while still reaping the benefits of reactive programming with streams. Purists, those who are very experienced with working with observables, and those working on smaller apps may not care as much about taking advantage of that tooling as using an elegant streams-only based solution, and that's fine. The important thing is having a choice.

Why redux-most or redux-observable over redux-saga?

redux-saga is nice. It's a sophisticated approach to handling asynchronous actions with Redux and can handle very complicated tasks with ease. However, due to generators being pull-based, it is much more imperative in nature. I simply prefer the more declarative style of push-based streams & reactive programming.

Differences between redux-most & redux-observable

Summary

  • There are no adapters. redux-most is only intended to be used with Most.
  • redux-most offers 2 separate APIs: a redux-observable-like API, where Epics get passed an action stream & a store middleware object containing dispatch & getState methods, and a stricter, more declarative API, where Epics get passed an action stream & a state stream.
  • combineEpics takes in an array of epics instead of multiple arguments.
  • Standard Most streams are used instead of a custom Observable extension.
  • select and selectArray are available instead of the variadic ofType.

Further Elaboration:

As the name implies, redux-most does not offer adapters for use with other reactive programming libraries that implement the Observable type. It's merely an implementation of redux-observable's "Epic" pattern exclusively intended for use with Most. Most is arguably the fastest, simplest, most functional, & most elegant reactive programming library in the JavaScript ecosystem right now, and Most 2.0 will be even better, as it will feature an auto-curried API like lodash/fp and ramda, but for working with streams instead of arrays. For a preview of what's to come, check out what's going on here.

Initially, redux-most offered the same API as redux-observable, where Epics received an action stream & a store middleware object containing dispatch & getState methods. However, it now offers both that API and another stricter, more declarative API which eliminates the use of dispatch & getState. The reason for this is that I rarely found myself using the imperative dispatch method. It's not really needed, because you can use switch, merge, mergeArray, etc. to send multiple actions through your outgoing stream. This is nice, because it allows you to stay locked into the declarative programming style the entire time.

However, using getState was still required in epics that needed access to the current state. I wanted a nice, convenient way to access the current state, just like I had for dispatching actions. So, I created an alternate API where Epics receive a stream of state changes rather than the { dispatch, getState } object. This state stream, combined with the new withState utility function, let's you use streams for both dispatching actions & accessing the current state, allowing you to stay focused & in the zone (the reactive programming mindset).

Moving on, whereas comebineEpics is variadic in redux-observable, it's unary in redux-most. It takes in only one argument, an array of epics, instead of individual epics getting passed in as separate arguments.

As for streams, I chose not to extend the Observable type with a custom ActionsObservable type. So, when working with redux-most, you will be working with normal most streams without any special extension methods. However, I have offered something similar to redux-observable's ofType operator in redux-most with the select and selectArray helper functions.

Like ofType, select and selectArray are convenience utilities for filtering actions by a specific type or types. In redux-observable, ofType can optionally take multiple action types to filter on. In redux-most, we want to be more explicit, as it is generally a good practice in functional programming to prefer a known number of arguments over a variable amount of arguments. Therefore, select is used when we want to filter by a single action type, and selectArray is used when we want to filter by multiple action types (via an array) simultaneously.

Additionally, to better align with the Most API, and because these functions take a known number of arguments, select & selectArray are curried, which allows them to be used in either a fluent style or a more functional style which enables the use of further currying, partial application, & functional composition.

To use the fluent style, just use Most's thru operator to pass the stream through to select/selectArray as the 2nd argument.

// Fluent style
const filteredAction$ = action$.thru(select(SOME_ACTION_TYPE))
const filteredActions$ = action$.thru(selectArray([SOME_ACTION_TYPE, SOME_OTHER_ACTION_TYPE]))

Otherwise, simply directly pass the stream as the 2nd argument.

// Functional style
const filteredAction$ = select(SOME_ACTION_TYPE, action$)
const filteredActions$ = selectArray([SOME_ACTION_TYPE, SOME_OTHER_ACTION_TYPE], action$)

Alternatively, you can delay passing the 2nd argument while defining functional pipelines via functional composition by using the compose or pipe functions from your favorite FP library, like ramda or lodash/fp. Again, this is because select & selectArray are auto-curried. Being able to program in this very functional & Pointfree style is one of the main reasons why someone might prefer using redux-most over redux-observable.

// Functional & Pointfree style using currying & functional composition
import { compose, curry, pipe } from 'ramda'
import { debounce, filter, map } from 'most'

// NOTE: Most 2.0 will feature auto-curried functions, but right now we must curry them manually.
const curriedDebounce = curry(debounce)
const curriedFilter = curry(filter)
const curriedMap = curry(map)

// someEpic is a new function which is still awaiting one argument, the action$
const someEpic = compose(
  curriedMap(someFunction),
  curriedDebounce(800),
  select(SOME_ACTION_TYPE)
)

// someOtherEpic is a new function which is still awaiting one argument, the action$
// pipe is the same as compose, but read from left-to-right rather than right-to-left.
const someOtherEpic = pipe(
  selectArray([SOME_ACTION_TYPE, SOME_OTHER_ACTION_TYPE]),
  curriedFilter(somePredicate),
  curriedMap(someFunction)
)

API Reference


createEpicMiddleware (rootEpic)

createEpicMiddleware is used to create an instance of the actual redux-most middleware. You provide a single root Epic.

Arguments

  1. rootEpic (Epic): The root Epic.

Returns

(MiddlewareAPI): An instance of the redux-most middleware.

Example

// redux/configureStore.js

import { createStore, applyMiddleware, compose } from 'redux'
import { createEpicMiddleware } from 'redux-most'
import { rootEpic, rootReducer } from './modules/root'

const epicMiddleware = createEpicMiddleware(rootEpic)

export default function configureStore() {
  const store = createStore(
    rootReducer,
    applyMiddleware(epicMiddleware)
  )

  return store
}

createStateStreamEnhancer (epicMiddleware)

createStateStreamEnhancer is used to access redux-most's alternate API, which passes Epics a state stream (Ex: state$) instead of the { dispatch, getState } store MiddlewareAPI object. You must provide an instance of the EpicMiddleware, and the resulting function must be applied AFTER using redux's applyMiddleware if also using other middleware.

Arguments

  1. rootEpic (Epic): The root Epic.

Returns

(MiddlewareAPI): An enhanced instance of the redux-most middleware, exposing a stream of state change values.

Example

import { createStore, applyMiddleware } from 'redux'
import {
  createEpicMiddleware,
  createStateStreamEnhancer,
} from 'redux-most'
import rootEpic from '../epics'

const epicMiddleware = createEpicMiddleware(rootEpic)
const middleware = [...] // other middleware here
const storeEnhancers = compose(
  createStateStreamEnhancer(epicMiddleware),
  applyMiddleware(...middleware)
)

const store = createStore(rootReducer, storeEnhancers)

combineEpics (epicsArray)

combineEpics, as the name suggests, allows you to pass in an array of epics and combine them into a single one.

Arguments

  1. epicsArray (Epic[]): The array of epics to combine into one root epic.

Returns

(Epic): An Epic that merges the output of every Epic provided and passes along the redux store as arguments.

Example

// epics/index.js

import { combineEpics } from 'redux-most'
import searchUsersDebounced from './searchUsersDebounced'
import searchUsers from './searchUsers'
import clearSearchResults from './clearSearchResults'
import fetchReposByUser from './fetchReposByUser'
import adminAccess from './adminAccess'

const rootEpic = combineEpics([
  searchUsersDebounced,
  searchUsers,
  clearSearchResults,
  fetchReposByUser,
  adminAccess,
])

export default rootEpic

EpicMiddleware

An instance of the redux-most middleware.

To create it, pass your root Epic to createEpicMiddleware.

Methods

replaceEpic (nextEpic)

Replaces the epic currently used by the middleware.

It is an advanced API. You might need this if your app implements code splitting and you want to load some of the epics dynamically or you're using hot reloading.

Example

import { createEpicMiddleware } from 'redux-most'
import rootEpic from '../epics'

...

const epicMiddleware = createEpicMiddleware(rootEpic)

...

// hot reload epics
const replaceRootEpic = () => {
  import('../epics').then(
    ({ default: nextRootEpic }) => { epicMiddleware.replaceEpic(nextRootEpic) }
  )
}

if (module.hot) {
  module.hot.accept('../epics', replaceRootEpic)
}

Arguments

  1. nextEpic (Epic): The next epic for the middleware to use.

select (actionType, stream)

A helper function for filtering the stream of actions by a single action type.

Arguments

  1. actionType (string): The type of action to filter by.
  2. stream (Stream): The stream of actions you are filtering. Ex: actions$.

Returns

(Stream): A new, filtered stream holding only the actions corresponding to the action type passed to select.

The select operator is curried, allowing you to use a fluent or functional style.

Examples

// Fluent style

import { SEARCHED_USERS_DEBOUNCED } from '../constants/ActionTypes'
import { clearSearchResults } from '../actions'
import { select } from 'redux-most'

const whereEmpty = ({ payload: { query } }) => !query

const clear = action$ =>
  action$.thru(select(SEARCHED_USERS_DEBOUNCED))
    .filter(whereEmpty)
    .map(clearSearchResults)

export default clear
// Functional style

import { SEARCHED_USERS_DEBOUNCED } from '../constants/ActionTypes'
import { clearSearchResults } from '../actions'
import { select } from 'redux-most'

const whereEmpty = ({ payload: { query } }) => !query

const clear = action$ => {
  const search$ = select(SEARCHED_USERS_DEBOUNCED, action$)
  const emptySearch$ = filter(whereEmpty, search$)
  return map(clearSearchResults, emptySearch$)
}

export default clear
// Functional & Pointfree style using functional composition

import { SEARCHED_USERS_DEBOUNCED } from '../constants/ActionTypes'
import { clearSearchResults } from '../actions'
import { select } from 'redux-most'
import {
  curriedFilter as filter,
  curriedMap as map,
} from '../utils'
import { compose } from 'ramda'

const whereEmpty = ({ payload: { query } }) => !query

const clear = compose(
  map(clearSearchResults),
  filter(whereEmpty),
  select(SEARCHED_USERS_DEBOUNCED)
)

export default clear

selectArray (actionTypes, stream)

A helper function for filtering the stream of actions by an array of action types.

Arguments

  1. actionTypes (string[]): An array of action types to filter by.
  2. stream (Stream): The stream of actions you are filtering. Ex: actions$.

Returns

(Stream): A new, filtered stream holding only the actions corresponding to the action types passed to selectArray.

The selectArray operator is curried, allowing you to use a fluent or functional style.

Examples

// Fluent style

import {
  SEARCHED_USERS,
  SEARCHED_USERS_DEBOUNCED,
} from '../constants/ActionTypes'
import { clearSearchResults } from '../actions'
import { selectArray } from 'redux-most'

const whereEmpty = ({ payload: { query } }) => !query

const clear = action$ =>
  action$.thru(selectArray([
      SEARCHED_USERS,
      SEARCHED_USERS_DEBOUNCED,
    ]))
    .filter(whereEmpty)
    .map(clearSearchResults)

export default clear
// Functional style

import {
  SEARCHED_USERS,
  SEARCHED_USERS_DEBOUNCED,
} from '../constants/ActionTypes'
import { clearSearchResults } from '../actions'
import { selectArray } from 'redux-most'

const whereEmpty = ({ payload: { query } }) => !query

const clear = action$ => {
  const search$ = selectArray([
    SEARCHED_USERS,
    SEARCHED_USERS_DEBOUNCED,
  ], action$)
  const emptySearch$ = filter(whereEmpty, search$)
  return map(clearSearchResults, emptySearch$)
}

export default clear
// Functional & Pointfree style using functional composition

import {
  SEARCHED_USERS,
  SEARCHED_USERS_DEBOUNCED,
} from '../constants/ActionTypes'
import { clearSearchResults } from '../actions'
import { selectArray } from 'redux-most'
import {
  curriedFilter as filter,
  curriedMap as map,
} from '../utils'
import { compose } from 'ramda'

const whereEmpty = ({ payload: { query } }) => !query

const clear = compose(
  map(clearSearchResults),
  filter(whereEmpty),
  selectArray([
    SEARCHED_USERS,
    SEARCHED_USERS_DEBOUNCED,
  ])
)

export default clear

withState (stateStream, actionStream)

A utility function for use with redux-most's optional state stream API. This provides a convenient way to sample the latest state change value. Note: accessing the alternate API requires using createStateStreamEnhancer.

Arguments

  1. stateStream (Stream): The state stream provided by redux-most's alternate API.
  2. actionStream (Stream): The filtered stream of action events used to trigger sampling of the latest state. (Ex: actions$).

Returns

([state, action]): An Array of length 2 (or Tuple) containing the latest state value at index 0 and the latest action of the filtered action stream at index 1.

withState is curried, allowing you to pass in the state stream & action stream together, at the same time, or separately, delaying passing in the action stream. This provides the user extra flexibility, allowing it to easily be used within functional composition pipelines.

Examples

import { select, withState } from 'redux-most'
import { curriedMap as map } from '../utils'
import compose from 'ramda/src/compose'

const accessStateFromArray = ([state, action]) => ({
  type: 'ACCESS_STATE',
  payload: {
    latestState: state,
    accessedByAction: action,
  },
})

// dispatch { type: 'STATE_STREAM_TEST' } in Redux DevTools to test
const stateStreamTest = (action$, state$) => compose(
 map(accessStateFromArray),
 withState(state$),
 select('STATE_STREAM_TEST')
)(action$)

export default stateStreamTest

redux-most's People

Contributors

joshburgess avatar jshthornton avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

redux-most's Issues

Rewrite createEpicMiddleware without Subjects to simplify and shrink build (removes replaceEpic)

Rewrite createEpicMiddleware without Subjects to simplify and shrink build (removes replaceEpic)

I talked with @davidchase about rewriting createEpicMiddleware without Subjects, and it looks like we can simplify the code a lot just by changing createEpicMiddleware to:

export const createEpicMiddleware = epic => store => next => action => {
  const actionsIn$ = just(action)
  const actionsOut$ = switchMap(x => epic(just(x), store), actionsIn$)

  observe(store.dispatch, actionsOut$)

  return next(action)
}

Pros:

  • Much simpler code
  • We are able to shrink the build size by quite a bit by eliminating the use of most-subject

Cons:

  • I don't think there is a way to create a replaceEpic function without using Subjects.

The redux-observable docs say that replaceEpic can be used for hot reloading & code splitting, but I haven't been using it. Do any of you use it? Can you provide some examples of how you're using it?

Do people think it's worth removing replaceEpic to get a smaller build size?

@jshthornton @vlad-zhukov @evilsoft

Does not work with Typescript 3.4

We've just updated to the latest TS version and now our build fails with the following error:

node_modules/redux-most/index.d.ts:27:32 - error TS2344: Type 'Dispatch<A>' does not satisfy the constraint 'Dispatch<AnyAction>'.
  Type 'AnyAction' is not assignable to type 'A'.

27   middlewareApi: MiddlewareAPI<Dispatch<A>, S>,

I downgraded TS to the last non-3.4 version (I think it was 3.3.4000) and it worked fine. Presumably TS is now picking up on something that was missed before...

Updating to @most/core

Big fan of this library, thanks for all the efforts.
@most/core provides curried functions and a tree-shakeable package, should we consider updating?
Plus updating from @most/core to most 2.0 will be non-breaking.

I will be happy to send a PR :).

Document 'select' behavior

Calling select (and I assume selectAny as well) returns a stream that can be chained further as follows:

select(EPIC_ACTION, action$)
  .concatMap(...)
  1. Should this behavior be mentioned in the readme?
  2. Shouldn't it be preferred to $action.thru(select(EPIC_ACTION))? It looks cleaner and I suppose it's a bit faster since less stuff happens.

Thoughts?

Passing non-functions to combineEpics() results in unhandled promise rejection.

It shouldn't normally happen, and in node 8 unhandled promise rejections are going to terminate the process.

As a fix I propose to ignore non-function values, this will also help a lot with code splitting/hmr and replaceEpic(). Another way is to catch the error in the stream with recoverWith(), but I don't see the right way how to do it here.

Consider switching from Webpack to Rollup

Consider Switching from Webpack to Rollup

I attempted this once in the past and ended up with a larger build size than I was getting with Webpack at the time and just gave up on it, but I may have been doing something wrong.

I'd like to try it again soon and see if I can get things working properly.

"Dispatching while constructing middleware" error with Redux ^4.0.0

When using the createEpicMiddleware with Redux applyMiddleware I get an error when creating the store:
Uncaught Error: Dispatching while constructing your middleware is not allowed. Other middleware would not be applied to this dispatch.
redux-most can't be used with Redux ^4.0.0 before this is fixed somehow.

Setup gitter community

It would be good to have a few gitter channels for things such as support, general discussion, and what not.

Add the ability to inject a custom argument into Epics

Possible Feature: Add the ability to inject a custom argument into Epics

Consider adding the ability to inject a custom argument into Epics to give access to an API utility or similar dependency, like currently possible with redux-thunk and redux-observable.

We will need to consider how we handle #4 when implementing this.

Async subject causes incorrect behaviour of an input caret

I've built simplest possible example, you can find it here: https://codesandbox.io/s/1y8rlnlp83

It looks like after an action is dispatched, actual emit of an action$ source happens in the next task, which causes the store to be updated too late. That causes React to directly set a value property of an input, which makes the caret to jump at the end.

An issue is neither with most itself, nor with react. I suppose the problem is in the async nature of most-subject.

Proposal: Have the actions argument be the last argument

In monadic programming the iteratee is provided last. I am proposing that actions$ is provided as the last argument and store is first. This could be achieved by checking the length of the provided function to the middleware, then providing the store if it is > 1.

The other option would be to make it via high order functions, whereby you'd have a withStore. Which would essentially do it for you.

Example syntax of both solutions:

const storeEpic = (store, actions$) => ...
const nonstoreEpic = (actions$) => ...
import { withStore } from 'redux-most';

const storeEpic = withStore((store, actions$) => ...);
const nonstoreEpic = (actions$) => ...

Add and start using a Continuous Integration tool

Add and start using a Continuous Integration tool

Requirement: must be a free option for open source

Potential options:

  • CodeShip
  • Jenkins
  • CircleCI (Does this provide any free version for open source projects yet?)
  • Travis
  • Any others?

TypeScript error with Redux 4

In Redux 4.0, I get the following TS error when compiling:
Type 'S' does not satisfy the constraint 'Dispatch<AnyAction>'

Apparently the MiddlewareAPI signature changed to MiddlewareAPI<Dispatch, S>

Append an epic to an in-progress epic

I’m looking for a way to append epics to the epic middleware.

I’ve experimented with replaceEpic but this effectively restarts all of your epics, so any epics in an “ongoing” state are just abandoned and any actions they emit are lost. Similarly, if I have an epic that utilises periodic or or event listeners, I am forced to kick start them again following a replaceEpic call.

Is there some way to persist your currently-running epics whilst adding new ones?

Not working without redux-thunk

I followed the docs completely and always got an error 'actions must be plain objects', but if I put thunk with epicMiddlewre in applyMiddleware everything works

1
2

redux-thunk instead of actions

If used with redux-thunk I got functions in stream instead of dispatched actions. Used same way as in docs with createStateStreamEnhancer

Proposal: Select over multiple action types

It is a common requirement to have to listen in to multiple actions for a given stream.

I propose a new operator called selectAny which is given an array of types as the first param. Then if any type matches it will filter through.

Change combineEpics to accept an array of epics

Breaking Change: Change combineEpics to accept an array of epics

I think we should change combineEpics to take in an array of epics instead of a variable amount of arguments. It would have slightly better performance, and it would get rid of the only remaining variadic function in the library.

You would just use it like

comebineEpics([
  epic1,
  epic2,
  epic3,
])

instead of the current

comebineEpics(
  epic1,
  epic2,
  epic3,
)

It's a very small change, but it is a breaking change to the API. Does anyone have any problems with this?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.