ngrx document reference resolution

Viewed 146

I try to implement a friend system using ngrx and firestore.

Therefore I created a collection /users holding the users information such as name, avatar picture link, online state etc. The second part consists of the /users/{id}/buddies collection for each individual user which holds the friends information only consisting of the friend's

  • user's id
  • buddy status which can be: pending requested or buddy.
  • reference to the user document

This setup allows to have a minimal collection for each users friends and not need to track a copy of the user information in the buddies table.

So much for the setup and context.

I have quite some issues with resolving the user document reference for an individual friend... ideally I want to have a buddies store which holds all the buddies of the current user and can be selected with various selectors.

The Buddy model should look something like:

export interface Buddy {
  uid: string;
  buddyStatus: BuddyStatus;
  userRef: User | any;
}

where the User model looks like:

export enum AuthState {
  LOGGED_IN = "LOGGED_IN",
  NOT_LOGGED_IN = "NOT_LOGGED_IN"
};

export interface User {
  uid: string | null;
  displayName: string | null;
  authState: AuthState;
  loading?: boolean;
  error?: string | null;
  avatarUrl: string | null;
};

The buddies store is synced to the firestore database by listening to the state changes and dispatching the corresponding ngrx store actions (added, modified, deleted):

  query$ = createEffect(() => this.actions$.pipe(
    ofType(BuddiesActions.query),
    switchMap(() => {
      return this.uid$.pipe(
        switchMap(uid => {
          return this.afs.collection(`users/${uid}/buddies`).stateChanges();
        })
      )
    }),
    mergeMap(actions => {
      return actions;
    }),

    // map the firestore actions to ngrx store actions to sync them
    // TODO this adds the user reference and things fuck up here...
    map((action: DocumentChangeAction<any>) => {

      const data = action.payload.doc.data();


      console.log({data: data});
      return {
        type: `[Buddies] ${action.type}`,
        buddy: {
          buddyStatus: data.buddyStatus,
          uid: data.uid,
          // this is the issue right here
          // userRef: data.userRef
        }
      };
    })
  ));

without the reference field the buddies are handled correctly but access to the buddies are crucial to display: online status, get the avatar url etc.

I manage to retrieve the buddies of a certain user synchronously but this is not really the way to go since I want to include changer to the user status etc.

I am fairly new to ngrx/angular and typescript and this is basically my first big project with these tools/framework/concepts.

I checked the other answers but most of them can resolve the reference synchronously but that's not really the goal since an observable to the user's information is wanted.

2 Answers

Here u go, There is a way that I have used to resolve this kind of issue in ngrx.

DO NOT listen on buddy changes when u do the changes on buddys. Put that listener as a separate one. Then perform any query on buddies when any actions triggered. No need to worry about immediate changes with action. Always fetch data from firestore and update local store.

 createEffect(() => this.actions$.pipe(
    ofType(BuddiesActions.query),
    switchMap(() => {
      return this.uid$.pipe(
        switchMap(uid => {
          return this.firestoreService.buddyQuery(uid)
        })
      )
    })

keep ur query related effects as it is and remove unwanted buddy date fetch calls there which fetch buddies data.

Then u need to introduce some action like 'listen on buddy changes' to listen on db changes.

 createEffect(() => this.actions$.pipe(
    ofType(BuddiesActions.Listen_ON_BUDDY_CHANGES),
    switchMap(() => {
   // add listner to firestore and trigger that observable when any data changes happend

  const data = action.payload.doc.data();

  console.log({data: data});
  return {
    type: `[Buddies] ${action.type}`,
    buddy: {
      buddyStatus: data.buddyStatus,
      uid: data.uid,
    }
  };

This should be an observable that listen on all the changes in firestore. when any changes happened there then update local ngrx store directly with that data. It is an asynchronous operation as u expected. Make sure to trigger that 'listen on buddy changes' action at the initial steps.

I currently use a workaround for the issue in the following manner:

On every update to the users/${id}/buddies collection I fetch the user in the query effect and assign it as a User object to the store:

  query$ = createEffect(() => this.actions$.pipe(
    ofType(BuddiesActions.query),
    switchMap(() => {
      return this.uid$.pipe(
        // map to state changes of the /users/{uid}/buddies collection
        switchMap(uid => {
          return this.firestoreService.buddyQuery(uid)
        })
      )
    }),
    mergeMap(actions => {
      return actions;
    }),
    mergeMap(docChange => {
      const buddy = docChange.payload.doc.data() as Buddy;

      return this.firestoreService.getUser(buddy.uid).pipe(
        map(user => {

          // assign the user to the buddy
          buddy.userRef = user.data();

          // map to the buddies ngrx actions
          return {
            type: `[Buddies] ${docChange.type}`,
            buddy: buddy
          }
        })
      );
    })
  ));

the main flaw is that the User is not updated on changes to the respective document. I tried to add an Observable as a field in the Buddy interface but this contradicts the ngrx store idiom where the state should not be an observable but data.

EDIT: After spending some thought on it I guess it would be best to add a second store holding the buddies's user information and accessing it with the respective ids.

Related