-
-
Save acdlite/c7eb7ac45617d9332b60 to your computer and use it in GitHub Desktop.
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. | |
} |
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.
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.
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.
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?