ngxs-labs / firestore-plugin

Firestore plugin for NGXS
https://ngxs-firebase-plugin.netlify.com/
MIT License
20 stars 14 forks source link

Cannot figure out async workflow #35

Closed santekotturi closed 3 years ago

santekotturi commented 3 years ago

I need to login with firebase, use the auth uid to lookup a user doc, then lookup some data in subcollections based on data in the user doc.

I'm finding this flow really complicated using this plugin although I do love the tooling/events/handlers it provides.

Can you provide an example of an async flow

I have an AuthState, a UserState and then AppState (which contains the data from subcollections).

Essentially I dispatch a Login event from AuthState. Then, I get a uid back. At this point, my UserState looks like:

  ngxsOnInit() {
    this.ngxsFirestoreConnect.connect(GetUser, {
      to: (action) => this.userSvc.doc$(action.payload.uid)
    });
  }

  @Action(GetUser)
  getUser() {
    console.log('[User State] Getting User');
    // I can't do login related things in here because this doesn't actually have access to the user doc.        
 }

  @Action(StreamConnected(GetUser))
  streamConnected(
    ctx: StateContext<VillagerStateModel>,
    { action }: Connected<GetVillager>
  ) {
    console.log('[User State] Stream connected');
  }

  @Action(StreamEmitted(GetUser))
  streamEmitted(
    ctx: StateContext<UserStateModel>,
    { action, payload }: Emitted<GetUser, User>
  ) {
    console.log('[User State] successfully fetched User');
    // I can't do login related things in here because this fires after every user doc update.
    ctx.patchState({
      currentUser: payload,
    });
  }

I was hoping that Connected might give the first value but it doesn't.

  export interface Connected<T> {
    action: T;
}
export interface Emitted<T, U> {
    action: T;
    payload: U;
}
export interface Disconnected<T> {
    action: T;
}
export interface Errored<T> {
    action: T;
    error: any;
}

My component ends up looking like:

this.store.dispatch(new Login()).toPromise();
const uid = this.store.selectSnapshot(AuthState.uid);
this.dispatch(new GetUser({uid}).toPromise();
const user = this.store.selectSnapshot(UserState.currentUser);   // this doesnt always return a value
this.store.dispatch([ 
    new FetchData1({user.param}), 
    new FetchData2({user.param}), 
    new FetchData3({user.param}
   ]).subscribe(() => {
    this.route.navigate(['/home']);
})

Maybe there's some rxjs magic that would help. I'm hesistant to use this.actions$.pipe(ofActionSuccessful(GetUser)).subscribe( ... ) because if this registers when the user navigates to the login page, and then they leave the page to go signup instead, this handler could fire would would disrupt the signup flow.... unless I should be handling unsubscribe when the nav state changes.... seems like a lot of work for what should be rather simple.

I'd really appreciate any examples or thoughts on how to perform async ops with this lib. Thanks!

[EDIT] The pure NGXS way is to handle dispatching child actions from inside other actions but I find myself unable to use that pattern with this lib. My GetUser action doesn't actually "do" anything, the StreamEmitted handles all data coming through.

I tried linking Actions:

this.actions$.pipe(ofActionSuccessful(Login, StreamEmitted(GetUser)).subscribe(() => 
   // maybe this will reliably return a user doc?
   const user = this.store.selectSnapshot(UserState.currentUser); 
   })

But this emits after each Action completes, not after both complete which opens it up to run after every emission/change of the User Doc.

joaqcid commented 3 years ago

hi! you can include all you need to dispatch inside your Login action you can use exhaustMap to ensure actions are run sequentially, and read data that prior action produced

@Action(Login)
login(ctx: StateContext, action: Login){
  // login code
  return {login observable}.pipe(
    exhaustMap(()=>{
      const uid = this.store.selectSnapshot(...)
      this.store.dispatch(GetUser(uid))
    }),
    exhaustMap(()=>{
      const params = this.store.selectSnapshot(...)
      this.store.dispatch(GetData(params))
    })
  )
}