Download / redux-apis

Helpers for creating Redux-aware APIs.
Creative Commons Attribution 4.0 International
11 stars 0 forks source link

Add `getHandler` method to `Api` #3

Closed Download closed 8 years ago

Download commented 8 years ago

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:

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);
    const oldHandler = this.getHandler('SOME_ACTION');
    this.setHandler('SOME_ACTION', (state, action) => {
      const newState = oldHandler(state, action);
      return { ...newState, extraProp:'extra thingie' }});
  }
}
Download commented 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:

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' }));
  }
}
Download commented 8 years ago

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 ...

athanclark commented 8 years ago

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
  }
}
Download commented 8 years ago

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')());
    }
  });
}
Download commented 8 years ago

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.