matthewmueller / socrates

Small (8kb), batteries-included redux store to reduce boilerplate and promote good habits.
577 stars 14 forks source link

yield FSA support #9

Closed tj closed 8 years ago

tj commented 8 years ago

Might be cool to allow yielding FSA objects instead of (or in addition to) returning, making short work of recovery from optimistic actions:

...
try {
  yield removePet(pet) // remove pet from dom
  yield fetch(`/pet/${pet.id}`, { method: 'DELETE' }) // remove it for realz
  yield message('Pet has been removed')
} catch (err) {
  yield addPet(pet) // oh noeeee
  yield error('Failed to remove pet')
}

I've been using redux-multi but it would be nice to ditch most/all redux middleware. Only thing I don't like about this is that it makes it anti-async/await friendly with duel-purpose yielding, and it might be just unclear in general which is which.

matthewmueller commented 8 years ago

@tj I think this is a great idea since this would allow you to dispatch multiple actions (FETCHING, FETCHED, ERROR) from a single function, which to me seems like the least amount of cognitive strain.

One thing that's I think we're still experimenting with as a community is where exactly these side-effects should live:

This library kind of points you towards an older approach. Side-effects should live in well-tested isomorphic libraries, like isomorphic-fetch, this has the benefit of not requiring any new libraries to be built, but the downside is that the effects are less centralized. I think allowing yielding on FSA objects would make this existing approach better than how it currently is implemented.

I've also been playing around with an idea of creating a thin wrapper that centralizes isomorphic libraries, where things like DOM effects would be behind an API that allows you to easily swap implementations for different environments. Something like this:

dispatch(function * () {
  var res = yield dom.fetch('http://google.com')
  yield dom.document.cookie.set('fetch status', res.status)
  return { type: 'fetch status', payload: { name: res.status }}
})

I think in order for containing side-effects to be viable, it cannot be much harder than doing document.cookie['my cookie'] = 'some cookie', otherwise it's probably too much work for the gain (assuming you're trying to launch products quickly).

At the risk of spamming some other folks and hijacking this issue, I'd like to loop in a few other guys:

@ashaffer @yelouafi @gaearon

ashaffer commented 8 years ago

In the app i'm working on we're now using redux-flo alongside redux-effect's effect middleware, which seems to be working out pretty well so far. redux-flo does exactly what you're looking for, I think.

However, you may also want to check out koax, though you'd have to abandon redux to do it (but koax can emulate redux, just with generators as first-class middleware citizens).

I would be using koax, but i'm using vdux for rendering which uses the middleware stack for rendering things, which makes it very performance sensitive and unfortunately generators are still a several-fold slowdown over redux's middleware approach.

@joshrtay

As for the redux-effects vs. redux-saga thing, I really like what redux-saga is doing, but in my own applications that abstraction overhead that it offers has never really been useful. Maybe in more richly interactive applications the notion of long-running interactive daemons is necessary but I haven't really seen a need for it yet.

joshrtay commented 8 years ago

redux-flo basically does exactly what you're looking for @tj, except that it treats all objects as actions, not just FSA compliant objects. I've gone back and forth on this, but if you enforce FSA for actions, and assume everything else that is yielded is control flow, then you can do some pretty cool things.

import flow from 'redux-flo'
import fetchMiddleware, {fetch} from 'redux-effects-fetch'
import bind from '@f/bind-middleware'

let dispatch = bind([flow(), fetchMiddleware])

dispatch(function * () {
  //sync
  yield fetch('google.com') // google
  yield fetch('facebook.com') // facebook
  //parallel
  yield [fetch('heroku.com'), fetch('segment.io')] // [heroku, segment]

  yield {heroku: fetch('herok.com'), segment: fetch('segment.io')}

  return 'done'
}).then(res => res /* 'done' */)

The object parallel notation doesn't work in redux-flo right now, but if we switched to FSA, enabling that would be trivial.

tj commented 8 years ago

Hmm redux-flo does look close, it's not super clear how it integrates with Redux since the examples don't use actions. Personally I'd like to avoid stuff like let dispatch = bind([flow(), fetchMiddleware]), I'm finding there's way too much glue logic in my apps.

For me one benefit of not having the side-effects in middleware, is that I already have action logic split into like 5 places haha, so one less place is fantastic. The other reason being we can use the same libs that we always use for imperative logic which is nice, even if it's not in the functional spirit. For common things like 'fetch' I think redux-effects + middleware is totally fine, but as soon as I need something custom I don't really want to write middleware.

ashaffer commented 8 years ago

@tj It integrates in the same way redux-effects does and can use the same effect middleware.

import flo from 'redux-flo'
import fetch from 'redux-effects-fetch'

const store = applyMiddleware(flo(), fetch)(createStore)(reducer, {})
import {fetch} from 'redux-effects-fetch'

function *fetchSomeThings () {
  try {
    const things = yield fetch('/some/things')
    yield gotSomeThings(things)
  } catch (err) {
    yield thingFetchError(err)
  }
}

store.dispatch(fetchSomeThings())

The docs should probably be updated to make that more clear though.

tj commented 8 years ago

ahh I see

joshrtay commented 8 years ago

ya sorry about the confusion there. @f/bind-middleware just creates a dispatch function from redux middleware. I use it for testing and on the server. I've been messing around with using some of these ideas in lambdas for example:

import flo from 'redux-flo'
import fetch from 'redux-effects-fetch'
import bind from '@f/bind-middleware'
import compose from '@f/compose'
import L from 'apex.js'

exports.handler = L(compose(bind([flo, fetch]), handler))

function * handler (e) {
  let things = yield fetch('/some/thing')
  return transformThings(things)
}
matthewmueller commented 8 years ago

closing, i'm no longer resolving anything. that's better done in an action log / effects library.