Closed Download closed 8 years ago
@athanclark proposed an addHandler
in Discord that would basically automate what we are doing in the example by calling oldHandler
from the new handler. So the complete API for manipulating handlers would look like this:
setHandler(action, handler)
getHandler(action, handler)
addHandler(action, handler)
clearHandler(action)
With this API, we could rewrite the example from above like this:
class Base extends Api {
constructor(state) {
super(state);
this.setHandler('SOME_ACTION', (state, action) => ({ ...state, someProp:action.payload }));
}
}
class Derived extends Base {
constructor(state) {
super(state);
this.addHandler('SOME_ACTION', (state, action) => ({ ...state, extraProp:'extra thingie' }));
}
}
Thinking about this more, maybe we should drop the method setHandler
and make addHandler
have the same effect as setHandler
when there are no handlers there yet. In those cases where you really want to completely replace the old handler, you could just call clearHandler
first before calling addHandler
. Yet another alternative could be to rename setHandler
to replaceHandler
...
Likewise I believe we should have an addHandler
function, to extend the effects (strictly later, in that the effect you're adding will be the last one in the aggregation so far - in how we aggregate, please see below in its section). This will allow for very useful components:
// a class that fetches data from some static url.
@remote
class LinkApi extends Api {
static INIT = { url: null, data: null, error: '', isResoved: false };
constructor(url, runOnInit = false, initState = LinkApi.INIT) {
const initState_ = R.merge(LinkApi.INIT, initState, { url: url })
super(initState_)
this.setHandler('SUCCESS', function handleSuccess(state, { payload }) {
return { ...state, data: payload, isResolved: true }
}))
this.setHandler('FAILURE', function handleFailure(state, { payload }) {
return { ...state, error: payload, isResolved: true }
}))
if (runOnInit) this.load()
}
load() {
this.fetch(this.getState().url)
.then(x => x.json())
.then(x => this.dispatch(this.createAction('SUCCESS')(x)))
.catch(e => this.dispatch(this.createAction('FAILURE')(e)))
}
}
Then later extend from it with our own behavior:
// an initialization component
@remote('http://mydomain.com/')
class AppLoaderApi extends LinkApi {
static INIT = R.merge(LinkApi.INIT, { readyData1: false, readyData2: false });
// concurrency lock in state
constructor(configUrl, initState = LinkApi.INIT) {
this.setHandler('READY', function ready(state, _) {
// this is where you can look at data1 and data2
// and know it's not null.
this.app.initialize(state.data1, state.data2) // not actually sure if you can do this in the current api
return state // initialization state isn't affected
})
this.setHandler('DATA1_READY', function ready1(state, { payload }) {
if (state.readyData2) this.dispatch(this.createAction('READY'))
return { ...state, readyData1: true }
})
this.setHandler('DATA2_READY', function ready2(state, { payload }) {
if (state.readyData1) this.dispatch(this.createAction('READY'))
return { ...state, readyData2: true }
})
// closing over substates
this.addHandler('data1/SUCCESS', function supply1(state, { payload }) {
this.dispatch(this.createAction('DATA1_READY')(payload))
return state // we don't need to affect LinkApi's already-done work
})
this.addHandler('data2/SUCCESS', function supply2(state, { payload }) {
this.dispatch(this.createAction('DATA2_READY')(payload))
return state // we don't need to affect LinkApi's already-done work
})
this.addHandler('SUCCESS', function buildData(state, { payload }) {
const config = payload
// fetches the links in parallel
this.data1 = link(this, new LinkApi(config.link1, true))
this.data2 = link(this, new LinkApi(config.link2, true))
this.app = link(this, new App()) // FIXME - the app needs to support a false-start.
// Ideally `App` should be some sort of parameter.
})
super(configUrl, true, initState) // load the config on start
}
}
this.dispatch
is not legal inside handlers. This is because handlers are called from the Api.reducer
function and redux does not allow dispatch
from inside a reducer. It could cause infinite loops.
Instead, if you need to dispatch an action, use redux-thunk
or such middleware.
I generally solve this by having very small handlers, that do the state manipulation only and have methods on the Api that dispatch thunks. Inside the thunk, I can then dispatch multiple actions, including those of the parent api.
i.s.o
this.setHandler('DATA1_READY', function ready1(state, { payload }) {
if (state.readyData2) this.dispatch(this.createAction('READY'))
return { ...state, readyData1: true }
})
A small handler, plus a method that dispatches a thunk:
this.setHandler('DATA1_READY', (state) ({ ...state, readyData1: true }));
//..
setData1Ready() {
return this.dispatch(() => {
this.dispatch(this.createAction('DATA1_READY')());
if (this.getState().readyData2) {
this.dispatch(this.createAction('READY')());
}
});
}
This is fixed. I kept it small and simple by only implementing getHandler
. It allows you to do everything discussed above and is the smallest Api that is still backwards compatible. Available from 1.0.0 and up.
We should be able to get the handler currently attached to an action, so we can use it from our extending class.
E.g. we want to be able to do this: