Search code examples
angularrxjsangular-servicesbehaviorsubjectapp-initializer

BehaviorSubject does not update when called from app.config.ts


TL;DR - Solution:

I was using subscribe rather than pipe the value to the APP_INITIALIZER. Also, I was registering twice UserService, once as a singleton (which it should be) and second time within the Header component it self, which basically was a new instance of UserService.

=======================

I'm calling my UserService from app.config.ts:

export const appConfig: ApplicationConfig = {
providers: [
    provideZoneChangeDetection({ eventCoalescing: true }),
    provideRouter(routes),
    provideHttpClient(),
    provideAnimationsAsync(),
    {
      provide: APP_INITIALIZER,
      useFactory: (userService: UserService) => {
        return () => userService.getSessionMock();
      },
      deps: [UserService],
      multi: true,
    },
  ],
};

UserService:

  public user$ = new BehaviorSubject<AuthUser | null>(null);

  constructor(private readonly http: HttpClient) {}

  public getSession() {
    return this.http
      .get<SessionUser>('https://localhost:7298/api/auth/session', {
        withCredentials: true,
      })
      .subscribe((user) => {
        (user as AuthUser).isAuthenticated = true;
        this.user$.next(user as AuthUser);
      });
  }

This doesn't work even when I'm using a mock of a user. It works when I run the code from ngOnInit on the component itself.

HeaderComponent which uses the data:

export class HeaderComponent {
  userService: UserService = inject(UserService);
  currentUser$ = this.userService.user$;

  ngOnInit() {
    this.currentUser$.subscribe((user) => console.log(user));
  }
}

header.component.html

<div *ngIf="currentUser$ | async as currentUser">
            <a href="https://localhost:7298/api/auth" *ngIf="!currentUser.isAuthenticated">
                <button class="bg-white text-cyan-500 px-4 py-2 rounded-full">Login</button>
            </a>
            @if(currentUser.isAuthenticated) {
            <span>{{currentUser.name}}</span>
            <a href="https://localhost:7298/api/auth/logout">
                <button class="bg-white text-cyan-500 px-4 py-2 rounded-full">Logout</button>
            </a>
            }
        </div>

getSessionMock

public getSessionMock() {
    this.user$.next(mockUser);
 }

Solution

  • You have to return the observable and not subscribe to it, also you can use tap to perform side effects inside the stream.

    Only then the user$ will be initialized before the application loads.

     public user$ = new BehaviorSubject<AuthUser | null>(null);
    
      constructor(private readonly http: HttpClient) {}
    
      public getSession() {
        return this.http
          .get<SessionUser>('https://localhost:7298/api/auth/session', {
            withCredentials: true,
          })
          .pipe(
            tap((user) => {
              (user as AuthUser).isAuthenticated = true;
              this.user$.next(user as AuthUser);
            })
          );
      }
    

    Also why are you calling getSessionMock shouldn't it be getSession.

    export const appConfig: ApplicationConfig = {
    providers: [
        provideZoneChangeDetection({ eventCoalescing: true }),
        provideRouter(routes),
        provideHttpClient(),
        provideAnimationsAsync(),
        {
          provide: APP_INITIALIZER,
          useFactory: (userService: UserService) => {
            return () => userService.getSession();
          },
          deps: [UserService],
          multi: true,
        },
      ],
    };