angular-redux / store

Angular 2+ bindings for Redux
MIT License
1.34k stars 202 forks source link

Global HTTP middleware vs epics #341

Closed ArielGueta closed 7 years ago

ArielGueta commented 7 years ago

Hey,

I saw in your recommendations and your flow that you are using redux-epics. I am wondering if there is a benefit to creating epic per "action/feature" compared to global HTTP epic middleware that will handle the requests?

Thanks.

e-schultz commented 7 years ago

I was thinking about something similar the other day, and also looking into:

I do like the idea of simplifying HTTP requests - I have been noticing a number of 'api' services that are doing nothing but the request - and no transformation of inputs/outputs because they are being handled in other parts of the application.

Instead of:

and repeat for each API service I have, I do like the idea of being able to

dispatch({type: 'API_REQUEST', payload: { 
url: '',
body: ''
method: 'get'
map: (result)=>  { map to what my app needs, } ,
actions: 'A_SUCCESS', 'A_ERROR' // actions out to this type?
})

One thing I'd be curious to see how it worked out, is calls that might need to make multiple requests (maybe some sync, some parallel).

I do think that Epics could coordinate this also, but 'old way', might be something like:

let req1 = someHttp1.get()
let req2 = someHtp2.get()
Observable
    .forkJoin(req1, req2)
    .switchMap(([res1,res2])=> someHttp3.get(res1.x,res1.y))

If you had a super-generic HTTP-middleware - as long as the 'success' types were distinct, could possibly write an epic like

fullObj = (action$) => { 
  Observable
    .forkJoin(action.ofType('SUCCESS_A'),
              action.ofType('SUCCESS_B'))
    .map(([res1,res2])=>Observable.of(
        {type: 'DO_3rd Call', url: `some/url/${res1.payload.id}/${res2.payload.id}`)
}

Would need to play around with the idea a bit more - for things that only need one API call to be fulfilled, makes sense - or, if a few API calls that don't rely on each-other - just the results of all of them.

Where my head starts to bend a bit - is when chained calls are needed, but I do think with a bit more thought/experimentation it could work out

ArielGueta commented 7 years ago

Thanks for the answer. I also don't understand what is the benefit of inject the action creator with the DI.

They are just plain objects, why not import them like any other ES2015 module? ( I saw this is what the ngrx/store guys do )

e-schultz commented 7 years ago

You don't need to use DI to get them, although a common pattern I was seeing in some projects is people wanting access to their other angular services in their action creators (mainly things dealing with Http) - and not wanting to put that logic into their components.

That said, the more I'm using something like redux-observable, the less need I have for access to DI in my action creators, and starting to go back to the plain function exports / plain JSON objects.

There is nothing saying you need to use @angular/http either - could use fetch/etc if you wanted.

ArielGueta commented 7 years ago

I just saw in the repo examples that this is how they are doing this even without injecting other things like CounterActions. https://github.com/angular-redux/store/blob/master/docs/intro-tutorial.md#actions

e-schultz commented 7 years ago

Theres a few ways you can handle action creators, and I've changed my opinion/preference a few times. There is also a bit of the 'react-redux' mindset/way, vs 'what feels natural to most Angular devs'.

What initially led me to going with ActionServices for awhile - as injectable things, not just imports, was dealing with async and wanting access to other providers/services.

Once they became injectable services - hey, can inject ngRedux, and have access to getState/dispatch from there also, and can drop redux-thunk.

@Injectable()
export class SomeAction {
  constructor(private someApi: SomeAPI, private ngRedux: NgRedux<State>) { }

  doSomething() {
    this.someApi
      .getStuff()
      .subscribe(result => this.ngRedux.dispatch({ type: 'GOT_STUFF', payload: result });
  }
}

@Component({/*stuff*/)
export class SomeComponent {
  @select(['stuff']) stuff$; 
  constructor(private someAction: SomeAction) { }

  doSomething() {
    this.someAction.doSomething(); 
    // instead of injecting redux, and ngRedux.dispatch(doSomething())
  }
}

this has a few pro's and con's - components don't need to worry about binding dispatch to actions, however it starts to encourage very fat action creators, since your not doing just a plain JSON object to return - slightly more complex to test. Could refactor out the thing that forms up the action into it's own function and test that.

For awhile I was a little bit hesitant about things like redux-saga or redux-observable, and felt like dealing with async there added too much indirection - and for smaller projects, think that may still hold true.

That said, I've become a big fan of redux-observable - and going towards that for coordinating streams, dealing with async/http, etc - and having my action creators be injectable services has less appeal, and favouring "just return an object, and don't bother with DI/etc". Although, this is preference and not prescriptive - and why there isn't anything about angular-redux that forces one way or the other.

In the end - it's all just middleware (epics are just middleware also), and being able to pick the ones that best fits your needs is a large part of why I like redux, and being able to leverage the existing redux ecosystem is a large part of the motivation behind angular-redux over alternatives like roll-your-own-redux, or ngrx/store.

ArielGueta commented 7 years ago

Great answer, thanks. My problem with redux is that there are so many ways to do the same thing. Some people like you take this as an advantage, but I think it's just confusing and adding more learning curve. I wish there were one particular style guide to "how to redux."

SethDavenport commented 7 years ago

Closing as an issue discussion. As for a global guide, yeah... :) Things change real fast though as you know.