DEV Community

Anthony G
Anthony G

Posted on • Edited on

Why is redux-observable Like That?

Three ducks redux

tl;dr

redux approximates the elm architecture w/o async capability. redux-loop and elm-ts both come close to exactly modeling elm, but redux-observable is simplest & most powerful. redux-thunk is naive, but popular enough that it influenced the design of redux

What's up with Redux?

redux is super weird. Why would we architect an application that way? How did someone even think to write it like that?

redux, redux-observable and react itself are the consequences of many concepts that have built on each other and in some cases been discovered in parallel. A couple of them come from backend design. Though each concept is simple, they form a complex web of relations to each other.

I gave up trying to come up with a coherent way to present this information, so I cheated. We're going to start at the present day and work ourselves backwards through the history of what led to redux, redux-observable and react. Hopefully this is a useful way to present information that's not necessarily linear. If you're unclear or confused about a concept, just keep reading, I might explain it later.

This is an opinionated view of what happened. Please let me know if you disagree with anything or if I left out anything important in the comments below!

(I'm also putting this article out against a deadline, so I'll be going back in and fleshing out some parts of this over time)

Timeline Overview

June 2020

Halogen 5 Released

Purescript Halogen gets its latest major release as of October 2020.

Halogen is a project initially created by Phil Freeman (original developer of the PureScript compiler) and funded by SlamData for use in their flagship app. So what is Halogen?

Elm and redux have a problem - they use a single sum type used to effect changes on a single global state. This is a problem because there are conceptual and performance advantages to colocated state (analogous article in react here).

Halogen, which is a pure framework that uses a component architecture similar to react, solved this problem by introducing sum types to handle communications between different components, called Query and Output.

The release of Halogen 5 introduces Action, which handles internal changees to a component's state. Presumably Action was named after redux's Action. This means that each component has its own conceptual redux, with three associated sum types:

  • Query goes from parent -> child, e.g. a parent component telling a video component to start playing
  • Message (AKA Output) goes from child -> parent, e.g. a dialog box component might want to invoke a dismissal function specified by its parent
  • Action goes from child -> child, e.g. a clicked button makes an ajax request

There's also:

  • Input which is analagous to react props

I mention this because Halogen is a logical extension of the ideas crystallized in redux and react. As such, redux and react can be simple frames through which to understand the complex Halogen architecture.

My theory is that Halogen was so named because of its adjacency to Functional Reactive Programming - in chemistry, the Halogens are characterized by their reactivity.

June 2017

fp-ts-rxjs Gives Observable a Formal Monad Instance

Giulio Canti's fp-ts-rxjs brings rxjs into the fp-ts ecosystem. It gives Observable an official monad instance, which it also transforms with Either for type-safe error handling and with Reader for dependency injection.

Also, it adds fromIO and fromTask operators, effectively turning redux-observable into an IO entry point for fp-ts programs using IO or Task.

March 2017

elm-ts Ports the Elm Architecture to Typescript

Giulio Canti's elm-ts proves that the Elm architecture can be represented in terms of react and rxjs. This makes it a saner alternative to redux + redux-loop.

The type signature of Cmd reveals it's strange frankenstein nature:

export interface Cmd<Msg> extends Observable<Task<Option<Msg>>> {}
Enter fullscreen mode Exit fullscreen mode

I view this as proof that redux-observable is simpler and more powerful than the Elm architecture.

In fact, since elm can't represent higher-kinded types (typescript can by way of fp-ts)), elm-ts is more powerful than elm itself.

November 2016

redux-pack Released

Only worth mentioning as an interesting approach to redux async middleware. Though it came out after redux-saga and redux-observable, it's interesting to think of a Promise of Action as a stepping stone to the idea of a stream of Action.

It was conceived because redux-saga was seen as unwieldy. I would say that the type safety and simplicity of redux-observable solves this problem without sacrificing any power.

May 2016

Elm Abandons Strict FRP

Elm gets rid of Signal, favoring Sub instead (short for 'Subscription'). Sub is similar to Observable, except with fewer combinators and operators. For more complex usage of Msg, Elm keeps Cmd, which it still uses at the time of writing.

In my opinion, by the very nature of the fact that Elm tries to model events, it was never truly FRP in the first place.

March 2016

rxjs Version 5 Released

rxjs version 5, spearheaded by Netflix's Ben Lesh, is released. It's goals are to increase performance and stack safety.

Lesh collaborates with Microsoft, and has the type system in Typescript had to be changed to accomodate rxjs. He also collaborates with Google, who make rxjs fundamental to Angular in their release of Angular 2.

Source

Feb 2016

redux-observable Released

Netflix releases redux-observable, which updates redux-saga's pull-based Iterable streams to more appropriate push-based Observable streams.

While pull-based streams are great for handling back-presure, they make less sense for event handling. To paraphrase Ben Lesh (a principal maintainer of rxjs), "imagine every individual mouse event was represented as a js promise".

Since redux is a front-end tool that handles events of one kind or another, a push-based architecture makes more sense.

It also provides a lush library of stream operators and combinators by way of Observable and rxjs. However, later this same month ixjs will be released, providing similar operators and combinators for Iterables.

The name epic is chosen to sound similar to yet different from saga.

redux-observable's logo is a circling loop colored similar to the ReactiveX electric eel. It loops because of the recursive nature of epics. They are three ducks because the Netflix team thought the word 'redux' sounded like 'three ducks'.

three ducks

Jan 2016

redux-loop released

redux-loop ports the Elm architecture to redux as an async middleware.

While a noble effort, it's a bit of a mess. Check out the type signature for Cmd (it's too complicated and involved to reprint here.) redux-observable's Epic is far simpler.

November 2015

redux-saga Released

redux-saga is an async middleware for redux that uses the 'saga' pattern. From the home page:

The mental model is that a saga is like a separate thread in your application that's solely responsible for side effects.

Earlier this year in July, Caitie McCaffrey gave a talk about her usage of the Saga pattern to develop Halo 4 at Microsoft. This was the inspiration, in part, for redux-saga.

July 2015

redux-thunk Released

The original async middleware for redux, cited in its documentation.

redux is so closely tied to redux-thunk that it changed its type signatures to accommodate it.

Let's examine the useDispatch hook in react-redux. Here's how you might expect it to work:

import { useDispatch } from 'react-redux'

const Comp = () => {
  const dispatch: (a: AppAction) => void = useDispatch<AppAction>()
  return (...)
}
Enter fullscreen mode Exit fullscreen mode

But here's how it actually works:

const dispatch: (a: AppAction) => void = useDispatch<(a: AppAction) => void>()
Enter fullscreen mode Exit fullscreen mode

What's up with that type signature? Why don't we just have to prove our Action type? Why do we have to provide the type signature for the dispatcher itself?

This is because with redux-thunk, it's possible to dispatch a function, so the type system has to be flexible enough to accomodate this. Here's an example of a function that might get dispatched (taken from the redux docs):

export function fetchPostsIfNeeded(subreddit) {
  // Note that the function also receives getState()
  // which lets you choose what to dispatch next.

  // This is useful for avoiding a network request if
  // a cached value is already available.

  return (dispatch, getState) => {
    if (shouldFetchPosts(getState(), subreddit)) {
      // Dispatch a thunk from thunk!
      return dispatch(fetchPosts(subreddit))
    } else {
      // Let the calling code know there's nothing to wait for.
      return Promise.resolve()
    }
  }
}

dispatch(fetchPostsIfNeeded('reactjs'))
Enter fullscreen mode Exit fullscreen mode

The anonymous function returned by fetchPostsIfNeeded is the titular 'thunk', although this is a misuse of the word - a 'thunk' is necessarily parameterless, since it represents a value that has already been 'thought through' and has no potential for change.

This is where redux starts to show its age. Though it was developed to bring functional concepts to javascript, it existed in a time before static typing with Typescript became popular. Some of its behavior is weird, and the type system has trouble keeping up with it.

Similarly, the type for store is

June 2015

Iterables, Generators Released as Part of ECMAScript 2015 (es6)

es6 saw an expansion on es4's Iterators with Iterables. An Iterable is a generalization of any pull based stream, pulled with the yeild keyword.

Iterables are not necessarily asynchronous: Arrays are Iterables too. This is part of the reason that reactive streams are not part of Functional Reactive Programming (FRP). A stream is by nature a sequence of discrete values, while FRP models continuous values as they relate to continuous time.

May 2015

redux is released

redux comes into being as a way to reconcile Dan Abramov's package react-hot-loader with Facebook's Flux architecture. It is explicitly based on the Elm architecture. It's introduced a couple months later in a popular talk titled "Live React: Hot Reloading with Time Travel"

At the time, Elm used something called a Signal. Signals composed together using a function called foldp:

foldp : (a -> s -> s) -> s -> Signal a -> Signal s
Enter fullscreen mode Exit fullscreen mode

This has three parts: the a -> s -> s is a function that takes an Action 'a' and the current state 's', and returns a new state 's'. The -> s after that represents the initial state. This is all then used convert a signal from its Action to new state.

Abramov, seeing this first part a -> s -> s recognized this as the function signature of es6 reduce. fold, when used on a product type in javascript, is called reduce. This word reduce, is the namesake of the reducer, which has a similar function signature.

The word reducer, combined with Flux, is the namesake of redux. The word 'redux' in English also means 'brought back, or revisited' - presumably, redux brings Flux back into the mainstream (?)

Part of the concept of reducers was that they'd be composable - a classically functional goal to be sure. Given that state is normally represented as a javascript object, each key of the object was to be given it's own reducer

import { combineReducers } from 'redux'

interface AppState {
  user: User
  current: Current
  settings: Settings
}
const userReducer: (u: User | undefined, a: Action) => User = ...
const currentReducer: (c: Current | undefined, a: Action) => Current = ...
const settingsReducer: (s: Settings | undefined, a: Action) => Settings = ...

const reducer: (s: AppState | undefined, a: Action) => AppState = combineReducers({
  user: userReducer,
  current: currentReducer,
  settings: settingsReducer
})
Enter fullscreen mode Exit fullscreen mode

Notice that each sub-reducer can be passed in an undefined state, but must always return a value. This is because, in keeping with the compositionality theme, each reducer is responsible for initializing its own data.

Unintuitively, redux will propagate an action called @@INIT through its reducer to collect this initialized state. In my opinion, this is a design flaw that can lead to unexpected behavior.

Despite all this, redux is a beautiful system. In the talk, redux is framed as bringing functional architecture to the mainstream. To quote:

These are not new things. I did not invent them. Some people will tell you that they existed 30 years ago, and maybe they'll be right, but we'll never find out if nobody talks about bringing cool functional techniques to javascript.

Because if you learn a cool programming language like Elm or Clojurescript, you stay there. It doesn't have to be this way.

You know, you can share your knowledge, please do. Because you can do functional in javascript. You can bring functional programming to the mainstream and do really cool stuff with it.

May 2014

Facebook open-sources Flux

Flux is facebook's implementation of MVC & CQRS on the client side. It's introduced in a talk called "Rethinking Web App Development at Facebook"

May 2013

Facebook open-sources React

In a surprisingly unpopular talk called "JS Apps at Facebook", Jordan Walke introduces react to the public.

Given its name, it seems that react came from 'Functional Reactive Programming'. However, the developers deny this:

The control over scheduling would be harder for us to gain if we let the user directly compose views with a “push” based paradigm common in some variations of Functional Reactive Programming ... There is an internal joke in the team that React should have been called “Schedule” because React does not want to be fully “reactive”.

March 2012

Elm released

Evan Czaplicki's Harvard undergrad thesis, named Elm, is released. The idea is to model a web UI using FRP in an accessible way.

The original paper documents different approaches to FRP & functional UI design through the 2010s. It's interesting to see how the ideas evolved over time.

It's easy to think that react took major inspiration from Elm. Here's a passage in the introduction from Elm's original paper:

Functional reactive programming is a declarative approach to GUI design ... With functional reactive programming, many of the irrelevant details are left to the compiler ... The term declarative is important because most current frameworks for graphical user interfaces are not declarative ... FRP also encourages a greatly simplified approach to graphical layout.

Sound familiar? Here's a quote from the react.js homepage:

React makes it painless to create interactive UIs. Design simple views for each state in your application, and React will efficiently update and render just the right components when your data changes ... Declarative views make your code more predictable and easier to debug.

Even if the name react was never meant to refer to FRP, clearly the ideas here are similar, yet both radical departures from the norm.

It is not clear, however, that react is at all based on Elm, as FaxJS and XHP have been in development for some time by now. I choose to believe that Elm and react were unaware of each other.

The two projects came to denotative design through radically different means: React from syntactic sugar by way of XHP, and Elm through academic rigor by way of frp. Like Hindley-Milner or Leibniz-Newton, the same concept was discovered twice. Phil Wadler, who brought monads into Haskell (building on Eugenio Moggi's work), often speaks of discovery as opposed to invention in mathematics in a similar way.

September 2011

Facebook Releases FaxJS

FaxJS is an early prototype of React.

var MainComponent = exports.MainComponent = F.Componentize({
  structure : function() {
    return Div({
      firstPerson: PersonDisplayer({
        name: 'Joe Johnson', age: 31,
        interests: 'hacking, eating, sleeping'
      }),

      secondPerson: PersonDisplayer({
        name: 'Sally Smith', age: 29,
        interests: 'biking, cooking swiming'
      }),

      thirdPerson: PersonDisplayer({
        name: 'Greg Winston', age: 25,
        interests: 'design and technology'
      })
    });
  }
});
Enter fullscreen mode Exit fullscreen mode

You can see the denotative nature of the component architecture here.

Sometime Early 2010 (?)

Microsoft Releases ReactiveX

ReactiveX, originally developed by Microsoft for their .NET system, models a stream called an Observable and gives it combinators and operations inspired by their own LINQ library. LINQ handles communications with databases. From their homepage:

Rx = Observables + LINQ + Schedulers

(Schedulers also exist in rxjs. They control the nature of the concurrency of an Observable)

Uses the same electric eel logo as the 2007 Microsoft project Volta because they were created by the same team.

The reactive electric eel

Date inferred from here. Earliest doc found here, in June 2011.

February 2010

Facebook Introuces XHP

XHP allows the direct usage of xml inside of php files.

if ($_POST['name']) {
  echo Hello, {$_POST['name']};
} else {
  echo
    <form method="post">
      What is your name?
      <input type="text" name="name" />
      <input type="submit" />
    </form>;
}
Enter fullscreen mode Exit fullscreen mode

This syntactic sugar is the precursor to JSX, whose denoatative nature will eventually make react so powerful.

June 1997

Conal Elliot and Paul Hudak Introduce FRP

At the International Conference on Functional Programming in the Netherlands, Conal Elliot and Paul Hudak introduce their paper titled "Functional Reactive Animation" (Fran for short). This idea will later be renamed Functional Reactive Programming (FRP).

Conal Elliot discusses the difference between FRP vs Reactive Programming in this SO post. FRP is by definition:

(a) denotative
(b) temporally continuous

Elliot also wrote a great post titled Why Program with Continuous Time, which is about pushing compromises to the boundaries of your system.

ReactiveX and FRP are fundamentally at odds: FRP's Behavior is by definition continuous while Observable is by definition discrete. FRP's Behavior is by definition pure (implied by denotative) while Observable is by definition effectful (thus fp-ts-rxjs's fromIO). They both model streams, but in their intentions they are exact opposites.

In the same SO post, Elliot talks about FRP as it relates to rxjs specifically:

As far as I can tell, Rx and Bacon.js lack both of the two fundamental properties on which I based the original FRP. In that sense, they're not what FRP set out to be. Which is fine with me, as I love to see a multitude of abstractions explored. Using a single term to describe them all, however, creates more confusion about what each means and how they differ. I think an accurate description of Rx and Bacon.js is "compositional event systems inspired by FRP".

It's a (really) bad example, but FRP as originally envisioned is closer to React Native's Animated library than to rxjs, Sagas or redux-observable. Continuous time is represented by an Animated.Value, and denotative transformations like Delay and Spring are applied before it's interpolated into actual positions at each frame. It doesn't look much like the Behaviors and Events described in the formal papers, but its continuous nature is closer in spirit than the discrete events output by Observable.

Elliot has said that hearing people refer incorrectly refer to non-FRP systems as FRP is 'heartbreaking for me personally'.

The terms 'Functional Reactive Programming' and 'ReactiveX' are confusingly similar - 'ReactiveX' debatably uses a functional paradigm ('functional' is a poorly defined term), and they're both technically Reactive Programming. They are antonyms masquerading as synonyms!

Ben Lesh (lead maintainer of rxjs) tweeted about this confusion - Elliot's reply:

Then perhaps better to discuss content first and names later.

Late 1980s

Haskell Team Invents & Discards redux observable

From Simon Peyton Jones's "Escape from the Ivory Tower"

A haskell program was simply a program from string to string ... This was a bit embarrassing. So after a bit we thought, maybe instead of producing a string, we'll produce a sequence of commands like "print hello," or "delete this file," which we can execute. So you can imagine, to run the program you call a program passing in some input and you get back a string of commands which some external command execution engine would - the evil operating system - the functional program is very good and pure and produces only a data structure, and the evil side-effecting operating system consumes these commands and executes them.

Well that's not very good if you want to read a file, because if you open a file and want to read it's contents, how does a program get access to it's contents? Ah! Maybe we can apply the function to the result of the evil external thing doing its work. So now there's this sort of loop between the pure functional program and the evil external operating system.

It didn't work very well at all. It was very embarrassing for a number of years.

Eventually, the solution that the Haskell team came to was the IO monad, as described in 1993's Imperative Functional Programming

Sometime 1988

CQS First Described by Bertrand Meyer

Command query responsibility segregation (CQRS) is a continuation of the principle proposed by Bertrand Meyer (1988) called command query separation (CQS)

2014 paper on CQRS

Earliest doc I could find on CQRS is from Greg Young's November 2011 CQRS Documents

Greg Young says CQRS is basically MVC

Jan 1987

Sagas First Described by Garcaa-Molrna and Salem

paper

Sagas relate to CQRS

December 1979

MVC Architecture Introduced by Trygve Reenskaug

paper

Conclusion

From an engineering standpoint, short of implementing an undo history, redux-observable is really a solution in search of a problem - it makes little sense. However, from a historical perspective, redux-observable is the logical conclusion of attempts at a purely functional frontend architecture, achieved by way of streams, CQRS and the Saga pattern. Hopefully it's easier to understand where and when redux-observable is an appropriate solution given a more whole understanding of its history.

I think deep understanding is a common difficulty in programming. React hooks are difficult to understand for many new developers. I would contend that it's easier to understand them if you have experience using setState, and have a concept for react components as fundamentally object-oriented (unfortunately).

However, even given this understanding, it was initially difficult for me to truly wrap my head around hooks. Why are there 'rules' that are impossible to enforce at compile time? Why do we need an entirely separate library to test them? Why is that weird one called useEffect? And how do they actually work?

It helped to learn that react hooks are actually a vastly simplified implementation of algebraic effects. This explains so much to me - hooks are an extreme approximation, so by definition flawed and strange. Indeed, Dan Abramov of the react team has written candidly that for him 'much discussion about algebraic effects is incomprehensible'. I don't mean to advocate against hooks or criticize Abramov - I think some hooks can be super useful, and I don't fully understand algebraic effects either. I only mean to say that a wholesale understanding of hooks and the complexity of its origins helps understand how best to use them (minimally).

The point is that often, mere documentation is not enough. Interfaces are abstractions, and all non-trivial abstractions leak to some degree.

This is true on the backend especially. For example, Apache Kafka is almost impossible to understand without first understanding the solutions that preceded it.

A similar idea is explored in the paper What we Talk About When We Talk About Monads They discuss this in context of monadic solutions that ended up not being useful. The paper posits that programming concepts ought to be thought of across three different levels

... treating monads in a more comprehensive way and considering the formal, implementation and metaphorical level could have prevented the undesirable use of monads

The paper suggests that developers often eschew metaphorical explanations of monads as 'kludges', but such an understanding is crucial and fundamental. I would argue that most redux-observable pedagogy is missing a different level of explanation.

In the spirit of the paper, we might say that the 'implementation' level of understanding redux-observable is its interface - the type signatures for reducer, dispatch, and epic. The 'metaphorical' level is the common understanding of redux-observable as a 'state management' library - something that can help wrangle complex state. These explanations are not incorrect, but they miss the crucial third level - 'formal'.

Formally, redux-observable is a referentially transparent functional architecture - an IO entry point. Its history is edifying because it took the long way to get there (as opposed to something like Elm). Understanding that journey can help understand some of the stranger points about it and some of the documentation and opinion pieces surrounding it.

Top comments (1)

Collapse
 
pianoboy profile image
pianoboy

Try it easy useSignal