2
votes

I'm sure this is an easy problem, I just don't know where to pinpoint it. I have an UserProfile service that creates a BehaviorSubject for the userdata. When I log in I use this service to broadcast the userdata. Once logged in I subscribe to this userdata in my Dashbord component. But the userdata observable is only returning the first empty object when I first created the BehaviorSubject. It is as if the login function that pushes the new data with .next() is only running after the dashboard subscribed to the BehaviourSubject. I tried putting the subscribe of my dashboard in ngAfterContentInit() so that the .next() runs before the subscription, but I'm still getting a blank object. If I run a .next() via set_new_data() function within my dashbord to check if the observable work, I get the data passed by .next(); But I don't get the data my login-component set via the set_userdata() function within my service.

Can we not share data like this using a service? Is the service destroyed after leaving the login component or what is happening?

My USerProfile Service:

@Injectable()
export class UserProfileService {
    public isLoggedIn: boolean = false;
    public userdata: BehaviorSubject<any> = new BehaviorSubject<any>({});

    constructor() {

    }

    public set_userdata(data) {
        // this.userdata = data;
        console.log(this.userdata);
        if (this.userdata === undefined) {
            console.log(data);
            //this.userdata = new BehaviorSubject<any[]>(data);
            console.log(this.userdata);
        } else {
            console.log(this.userdata);
            this.userdata.next(data);
        }

    }
}

My Dashbord Component:

@Component({
    selector: 'dashboard',
    styleUrls: ['./dashboard.scss'],
    templateUrl: './dashboard.html'
})
export class Dashboard {
    public isLoggedIn: boolean;
    public curr_user: object;

    constructor(private _appGlobals: AppGlobals, private _user: UserProfileService) {
        this._appGlobals.isUserLoggedIn.subscribe(value => this.isLoggedIn = value);
        // this.curr_user = _user.userdata;
    }

    ngAfterContentInit() {
        console.log("this executes first");
        this._user.userdata.subscribe(value => {
            this.curr_user = value;
            console.log(value);
        });
    }

    set_new_data() {
        let obj = {
            name: "test",
            age: 45
        };
        this._user.userdata.next(obj);
    }
}

My Console output, (shows empty object: {}, expected the userdata)

enter image description here

2
Where do you provide UserProfileService? Why do you have this line commented out in you code //this.userdata = new BehaviorSubject<any[]>(data);. It's safe to remove it completely. There shouldn't be any need to assign a new instance to it. This would invalidate all subscriptions.Günter Zöchbauer
I provide for it in the LoginModule and the DashbordModule. That commented line was because I was trying to get the initial instance to have the correct data when creating the BSubjectjohan
Is one of these modules lazy-loaded? To initialize the BehaviorSubject, just call this._userdata.next(someInitialValue);Günter Zöchbauer
I think it is lazy loaded, I am working on the github.com/akveo/ng2-admin framework. for the login path i have this decleration: loadChildren: 'app/pages/login/login.module#LoginModule'johan
Lazy-loaded modules create their own DI root scope. It's likely that there are 2 instances of your UserProfileService around. Try to only provide the service in AppModule and try again if you get the desired behavior. You can implement forRoot() and use forRoot() to import lazy loaded modules to ensure only a single service instance is created. angular.io/guide/ngmodule-faq#what-is-the-forroot-methodGünter Zöchbauer

2 Answers

3
votes

Ensure that you have only a single instance of your service, otherwise the subscriber might subscribe to a different BehaviorSubject than the one you use to emit the events with.

Providing a service on a component can result in as many service instances as instances of this component are created.

Another pitfall that can cause multiple instances are lazy-loaded modules which get their own DI root scope.
Providers from non-lazy-loaded modules are hoisted to the application-root scope, and even when multiple non-lazy-loaded modules providing the same service, there will be only a single instance.

Because DI scopes can't be updated after initialization, it's not possible to hoist providers from lazy-loaded modules into the application root scope, therefore a new "root" scope is created (as a child scope of the application root scope or another lazy-loaded "root" scope) for each set of lazy-loaded modules that are loaded together.

Using forRoot allows to add providers to the application root scope from lazy-loaded modules. See also https://angular.io/guide/ngmodule-faq#what-is-the-forroot-method,
or alternatively just provide the service in AppModule or any other module that is known to not be lazy-loaded directly.

0
votes

If you return userdata from set_userdata(data) as an Observable and then subscribe to that function instead of the Subject itself your code should work as expected. Although I am not sure about best practice;

//UserProfileService
public set_userdata(data) { //--> Maybe make this get_userdata()
    // this.userdata = data;
    console.log(this.userdata);
    if (this.userdata === undefined) {
        console.log(data);
        console.log(this.userdata);
    } else {
        console.log(this.userdata);
        this.userdata.next(data);
    }
    return this.userdata.asObservable();
}

//Dashboard
ngAfterContentInit() { //--> this could be in ngOnInit
    console.log("this executes first");
    this._user.set_userdata().subscribe(value => {
        this.curr_user = value;
        console.log(value);
    });
}