fluture-js / Fluture

🦋 Fantasy Land compliant (monadic) alternative to Promises

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Is it advised to stick a Fluture into the Redux state/store tree?

CMCDragonkai opened this issue · comments

I often have to create an Async type to reflect asynchronous operations in Redux so that components can render according to the async state. Like whether it's in-progress or completed or failed. Since failure of an asynchronous operation for UI-environments tends to require the user to trigger a recovery action directly. This is tedious to convert between promises to Async and promises cannot be observed to be in-progress. Could Fluture be the immutable asynchronous data type that I can pattern match and store in Redux to get the best of all worlds?

If I understand you correctly, then the short answer to your question is no. A Future itself does not represent the states that it can be in.

The long answer is that you could make a Redux middleware to handle the translation for you automatically.

const Future = require ('fluture')
const {taggedSum} = require ('daggy')

const Async = taggedSum ('Async', {
  Idle: [],
  Pending: ['cancel'],
  Rejected: ['reason'],
  Resolved: ['value'],
})

const flutureMiddleware = store => next => ({type, payload}) => {
  if (!Future.isFuture (payload)) return next ({type, payload})
  let settled = false
  next ({type, payload: Async.Idle})
  const cancel = payload.pipe (Future.fork (reason => {
    settled = true
    next ({type, payload: Async.Rejected (reason)})
  }) (value => {
    settled = true
    next ({type, payload: Async.Resolved (value)})
  }))
  if (!settled) next ({type, payload: Async.Pending (cancel)})
}

With this middleware installed, you'd be able to dispatch a Future like:

store.dispatch ({type: 'GET_DATA', payload: getDataUsingFluture ()})

...and the middleware will translate that to the data type you want, which will be received by your Redux store.


Note that it's common practice to ensure that your state tree is fully serializable. In this case we're making an exception for the cancel function returned by Fluture. When serializing an instance of Pending, you could first translate that to Rejected ('The state was saved while an async ation was pending'), or something.


To come back to your original question - I believe what you're looking for specifically is this Async type we defined. It might be nice for Fluture to provide you this type, with appropriate algebraic instances, through a separate module. With it, your middleware could look as simple as:

const Future = require ('fluture')
const {observe} = require ('fluture-state') // taking ideas for package name :p

const flutureMiddleware = store => next => ({type, payload}) => {
  if (!Future.isFuture (payload)) return next (action)
  observe (payload => next ({type, payload})) (payload)
}

What do you think?

That does sort of look like what I need, but I've never used middleware in that way. Does that mean that middleware is checking every single value being dispatched? That seems excessive. And what about the other end with the component reacting to the state transition?

Also why not add in the ability to synchronously check the state of the fluture? What is preventing that?

I just want to expand on the utility of this from the component perspective.

Sometimes you have a component that needs to render according to 2 different asynchronous resources.

So right now using my own Async type..

type Async<D, I = any, E = Error> =
  undefined | null
  |
  Readonly<{ type: 'Progress', id?: I }>
  |
  Readonly<{ type: 'Success', id?: I, data: D }>
  |
  Readonly<{ type: 'Fail', id?: I, error: E }>;

If I have a component with 2 async resources. I have 2^4 (16) possible states to render:

null, null
null, progress
null, success
null, fail
progress, null
progress, progress
progress, success
progress, fail
success, null
success, progress
success, success
success, fail
fail, null
fail, progress
fail, success
fail, fail

Usually this is just handled by 2 sub components rendering according to each resource.

However if the promise/fluture can be realized into Redux itself. It could be possible to coalesce fluture states together using combinators like Promise.all or the equivalent in Fluture.

For now it appears that to easily coalesce states together, I would have to use switch cases and fall through syntax.

I've never used middleware in that way

Middleware was designed, among other purposes, for async side effects:

People use Redux middleware for logging, crash reporting, talking to an asynchronous API, routing, and more. -- https://redux.js.org/advanced/middleware


Does that mean that middleware is checking every single value being dispatched? That seems excessive.

Let's do some quick back-of-the-envelope math to put it in perspective. Suppose that your user is using the front-end for 15 minutes. During that time, they manage to dispatch about 1000 actions (that's really a lot of user interactions).

This means that over the course of 15 minutes, the code is doing the equivalent of filtering a list of 1000 items by isFuture. Let's see how long that takes.

$ node -e 'var {isFuture}=require("fluture");console.time("t");Array.from({length:1000},(_,i)=>i).filter(isFuture);console.timeEnd("t")'
t: 0.482ms

Half a millisecond! That's 0.00006% of the total time you had during those 15 minutes. That's not the kind of performance hit we need to be concerned about.

Also why not add in the ability to synchronously check the state of the fluture? What is preventing that?

Because Futures are stateless. In order to inspect the "state" of an asynchronous computation, Fluture would have to write that state to somewhere, which goes against the stateless design. The best Fluture can do is give you a function to "observe" the state, which is what the const {observe} = require ('fluture-state') idea was about.

For now it appears that to easily coalesce states together, I would have to use switch cases and fall through syntax.

You can combine the two async computations before dispatching them to the store:

const MyComponent = ({dispatch, data}) => (
  <div>
    <button onClick={() => dispatch ({
      type: 'LOAD_DATA',
      // Here we combine the Futures using 'both'!
      payload: Future.both (loadThing ()) (loadOtherThing ())
    })}>Load the things!</button>
    {
      data.cata ({
        Idle: () => <p>Not doing anything.</p>,
        Pending: (cancel) => <p>Loading... <button onClick={cancel}>Abort!</button></p>,
        Rejected: (reason) => <p>Uh-oh; {reason}</p>,
        Resolved: ([thing, otherThing]) => (
          <div>
            <pre>{JSON.stringify(thing, null, 2)}</pre>
            <pre>{JSON.stringify(otherThing, null, 2)}</pre>
          </div>
        )
      })
    }
  </div>
);

Ok, sounds like having that library fluture-state will be a pretty useful. I might start using it if the workflow looks nice.

Ok, sounds like having that library fluture-state will be a pretty useful.

We have implemented it nearly completely in the first example I gave in #381 (comment). You should be able to achieve everything you need with that.

@CMCDragonkai I finally went ahead and created the helper library that was discussed: fluture-js/fluture-observe#1