Open rpominov opened 9 years ago
If you go that road (it might be fun!) the API might need to be a bit smarter. I haven't figured it out myself yet so for now I'm just dumping everything I need right into my dispatcher, but at the very least middleware must be powerful enough to express the concept of transactions.
Here's a working implementation of transactions (built into dispatcher—but I want to somehow turn this into middleware later on). Call dispatcher.transact()
at any moment, and the actions occurring after it can be committed by calling commit()
, or cancelled by calling rollback()
on the returned object. This is not the most interesting part. If a hot reload occurs while a transaction is active, the staged actions will be replayed on top of the last committed state. Sorry if this doesn't make sense—as redux grows more mature, I'll make some demos. But for now you can
npm start
increment
actiontx = dispatcher.transact()
from DevTools consoleTransactions let you replay a few last actions on each hot reload so that you can fine-tune your Store code until the actions are handled correctly. I'm positive that any middleware system needs to have all the necessary hooks to make this possible.
Hm, I see how transactions work, but don't quite understand the use case. Where one will call dispatcher.transact()
and transaction.commit()
(in stores, action creators, app code?), and what kind of actions should be done in a transaction?
It is meant to be done from a devtool. Imagine a devtool (like Chrome DevTools extension) that lets you go back to arbitrary state, or to mark a certain state as "committed" and have hot reload only re-execute a few actions you're currently debugging.
Oh, I see, if we do something like this http://www.youtube.com/watch?v=Fo86aiBoomE Yes, transactions could be useful. Need to think about it.
Have an idea, the API may look like this:
const fluce = createFluce(({dispatch, replaceState, reducer}) => {
// In middleware you're given an object with functions,
// and must return an object with same shape. If you are not modifying
// a function, put the original one to the output object.
//
// This middleware doesn't do anything, it only shows interface of the object.
return {
// This is called when someone call `fluce.dispathc(type, payload)`
// You can also call given `dispatch` function whenever you want.
dispatch({type, payload}, curState) {
dispatch({type, payload}, curState)
},
// This is called after `dispatch`, it replaces `fluce.stores`
// object with a new one, and notifies subscribers of changes.
// Again you can call given `replaceState` whenever you want.
replaceState(newState) {
replaceState(newState)
},
// This is used to apply an action to all stores to get new state.
// It is a pure function, you can use it to modify state object when needed.
// You can also change its behavior, but make sure it stays a pure function.
reducer(curState, {type, payload}) {
return reducer(curState, {type, payload})
}
}
})
This is how transactions could be implemented using it (totally untested):
function createTransactMiddleware() {
let _replaceState
let _reducer
let inTransaction = false
let capturedState
let capturedActions
return {
transact() {
if (inTransaction) {
throw new Error('can\'t nest transactions')
}
inTransacrion = true
capturedActions = []
return {
commit() {
inTransaction = false
capturedState = undefined
capturedActions = undefined
},
cancel() {
if (capturedState) {
_replaceState(capturedState)
}
inTransaction = false
capturedState = undefined
capturedActions = undefined
},
replay() {
if (capturedState) {
_replaceState(capturedActions.reduce(_reducer, capturedState))
}
}
}
},
middleware({dispatch, replaceState, reducer}) {
_replaceState = replaceState
_reducer = reducer
return {
dispatch(action, curState) {
if (inTransaction) {
if (!capturedState) {
capturedState = curState
}
capturedActions.push(action)
}
dispatch(action, curState)
},
replaceState,
reducer
}
}
}
}
const transactionMw = createTransactMiddleware()
const fluce = createFluce(transactionMw.middleware)
// ...
let tr = transactionMw.transact()
// ...
tr.repaly()
tr.cancel()
I'm going to play with it more after I finished with React bindings (<Fluce />
, and connectStores
). Also going to implement fluce.optimisticallyDispatch as a middleware.
This sounds sensible!
Here goes one idea of how it could be done (not sure if it's good enough):
When creating a Fluce instance you can provide a middleware that will be used to change default fluce behavior regarding update stores state. A middleware is a function that returns another function:
A no op middleware is looks like this
(replaceState) => replaceState
, it won't change the default behavior.To set a middleware you need to pass it to
createFluce()
function:This feature allows you to implement advanced stuff like "time travelling" or "undo" from this prototype by @gaearon.
Perhaps we should also allow hooks on before action dispatch...