Skip to content

Instantly share code, notes, and snippets.

@acdlite
Last active August 29, 2015 14:20
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save acdlite/c7eb7ac45617d9332b60 to your computer and use it in GitHub Desktop.
Save acdlite/c7eb7ac45617d9332b60 to your computer and use it in GitHub Desktop.
Imagining a more functional, classless Flummox API
import Flummox from 'flummox';
import { Map } from 'immutable';
// Instead of a constructor, just wrap flux creation inside a function
export default function createFlux() {
const flux = new Flummox();
// Actions are the same. Just pass an object instead of a class.
const thingActions = flux.createActions('things', {
incrementSomething(amount) {
return amount;
},
async createThing(newThing) {
// async/await isn't strictly necsesary here -- we could just return the
// the promise, but I like the reminder that this is async
return await WebAPIUtils.createThing(newThing);
}
});
const thingActionIds = fooActions.getActionIds();
// Note that everything above is already possible in the current version
// of Flummox.
//
// Here's where things get interesting...
const thingStore = flux.createStore('things', {
getInitialState: () => ({
things: new Map(),
_pendingThings: new Map(),
counter: 0
}),
// Instead of `this.register()` et al, use hooks (like React's lifecycle
// hooks) to set up action handlers. `register()` should return a hash of
// action ids mapped to handlers.
register: () => ({
// Return new store state, given the previous state and the value sent by
// the action. Like with `setState()`, the new state is merged with the
// old state. It's like a transactional state update, or a
// reduce operation (except it's a merge, not a replace).
//
// All params are passed as one hash, to support possible future interop
// with RxJS. Yay for destructuring!
[thingActionIds.incrementSomething]: ({ prevState, value: amount }) => ({
counter: prevState.counter + amount
}),
// For async actions, use a sub-hash with success and failure handlers.
// The naming convention is taken from RxJS observers. Notice that there's
// no `onBegin`, which you might expect if you're used to
// Flummox's `registerAsync()`. For that, use the `registerOnStart()` hook
[thingActionIds.createThing]: {
onNext: ({ prevState, value: newThing, payload: { dispatchId } }) => ({
things: prevState.things.merge({
[newThing.id]: newThing
}),
_pendingThings: prevState.delete(dispatchId)
}),
onError: ({ payload: { dispatchId } }) => ({
_pendingThings: prevState.delete(dispatchId)
})
}
}),
// Specify handlers that fire at the beginning async actions, in order to
// perform optimistic updates
registerOnStart: () => ({
[thingActionIds.createThing]: ({ prevState, payload: { dispatchId, actionArgs: [ newThing ] }}) => ({
_pendingThings: prevState._pendingThings.merge({
[dispatchId]: newThing
})
})
})
});
// We could support both the class-based API and this new functional API
// without much fuss. I think this is important, since much of the appeal of
// Flummox comes from the familiarity and predictability of its API. Many
// people are not used to functional programming concepts, and will want to
// stick with classes.
}
@tappleby
Copy link

I agree with the combine syntax feeling unintuitive. I see your point with these almost being 2 separate streams but it still feels weird having related logic broken out into different sections.

If we zoom out at a higher level, what advantages does this syntax offer us? does following the observable contract allow for interop with other apis (RxJs) or is it more for a familiar interface?

@tappleby
Copy link

I think part of the issue is with async actions we have one actionId representing two separate streams (promises, resolved promise results).

I wonder if theres a way we could identify we want the promise stream instead of the resolved values:

{
  register: () => ({
    [thingActionIds.createThing.begin]: ...,
    [begin(thingActionIds.createThing)]: ...,
    [thingActionIds.createThing]: { onNext, onError }
  })
}

I use begin for lack of a better name.

@acdlite
Copy link
Author

acdlite commented May 12, 2015

That's a clever idea. Except action ids (constants) are strings, and you can't attach properties to them. But I like the notion of using a special key for the begin stream.

we want the promise stream instead of the resolved values

That's a better description of what I was trying to say above. The begin / start stream is essentially the result of a mapping transformation on the stream of promises, whereas the onNext stream is the result of a flatMap-ing transformation on the stream of promises.

@johanneslumpe
Copy link

I do understand why you want to separate them out into two sections, but just as for tappleby it feels a bit off for me. His syntax would allow us to at least keep things a bit closer together. How about something like this:

{
  register: () => ({
    [thingActionIds.createThing]: {
      begin() {...},
      stream: {onNext, onError}
    }
  })
}

But now that I've written that out, it feels weird having to nest the stuff like that. I'll leave it here for discussion anyway.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment