Comparison to Redux Observable - deanrad/rx-helper GitHub Wiki

RxHelper was born after many great conversations with folks in the Redux-Observable chat room, including @jayphelps, and @berkeleytrue. I was convinced that Redux-Observable made many cool things possible, but my very first question was "Why must this coolness live inside a Redux middleware?" Some time later, the essence of Redux-Observable was distilled into a Redux-less library called the Antares Protocol, which became this RxHelper library.

The Similarities:

  • Specify consequential, possibly async Flux Standard Actions in response to other actions.
  • Handle Async side-effects
  • Allow front-end apps like SPAs to focus on dispatching a single Action

The Differences:

  • You need Redux for Redux-Observable (Redux-Observable), you don't with RxHelper (a store can be added via a filter)
  • The async takes place inside the store, with Redux-Observable, outside with RxHelper.
  • The one calling .dispatch has ways of directly accessing the results of those async operations with RxHelper, not so with Redux-Observable
  • RxJS Concurrency is utilized in chained pipes with Redux-Observable, it's explicitly a configured parameter with RxHelper, thus read and understood and changed more readily

Vocabulary Differences

Naming is hard. But RxHelper has intentional naming that deviates from vanilla RxJS. This is either a big help to you, or this library's biggest shortcoming! Open an Issue or PR if you think a different set of vocabulary might work. I think something like this may work very well by internationalizing some of these terms into different languages. Source code legibility/comprehensibility benefits from code being written in a style that makes the most sense for your domain.

  • store.dispatchagent.process
  • Actions ⟺ Events
  • Epics ⟺ Renderers
  • store ⟺ agent
  • actionsOfType ⟺ agent.on(eventTypePattern)
  • mergeMapparallel
  • switchMapcutoff
  • concatMapserial
  • exhaustMapmute

Full Example

The Days Without Accident example, coded as a Redux Observable set of epics:

// rootEpic.js
const accidents = action$ =>
   action$
     .ofType('start')                 // Kicked off just once
     .mergeMap(() =>                       // unpack the events returned
       interval(1000*60*60*24)                 // Every day
         .delay(1000*60*60*12).                // at midday
         .filter(() => Math.random() < 0.01)   // if we're unlucky
         .map(() => ({                         // create the accident info
             date: new Date(),
             severity: Math.random().toString(7)[2]
         }))
        .map(payload => ({ type: 'accident', payload }))


const reports = action$ =>
   action$
     .filter({ type } => /start|accident/.match(type))                 // Kicked off on start, or every accident
     .switchMap(() =>                   // Restarting each new accident, and unpacking events
        interval(1000*60*60*24)         // Every day at the beginning of the day
           .scan(({count}) => ({        // Create a {date, count} Object
              date: new Date(),         //   with today's date
              count: count + 1          //   that increments
           }), {count: 0})

export default { accidents, reports }

// kicked off with store.dispatch({ type: 'start' })

And the same, as RxHelper code. Note a higher signal/noise, and the prominence that cutoff mode is being used, vs the switchMap operator, embedded in the call chain or pipe:

agent.on('start', () => {
    return interval(1000*60*60*24)           // Every day
       .delay(1000*60*60*12).                // at midday
       .filter(() => Math.random() < 0.01)   // if we're unlucky
       .map(() => ({                         // create the accident info
           date: new Date(),
           severity: Math.random().toString(7)[2]
       }))
}, {
  type: 'accident'                           // And add it to the Action Stream as type: 'accident'
})

agent.on(/start|accident/, () => {        // Upon an accident *or* the start of the app
    return interval(1000*60*60*24)
       .scan(({count}) => ({             // Create a {date, count} Object
           date: new Date(),             //   with today's date
           count: count + 1              //   that increments
       }), {count: 0})                   //   starting at zero
}, {
    type: 'dailyReport',
    concurrency: 'cutoff'                // Replace the last counter, starting a new one (switchMap)
}

// kicked off via agent.process({ type: 'start' })

// If you want all events to go through a store as well:
agent.filter(/.*/, ({ event }) => store.dispatch(event))