1
votes

I trying to use a data service in a ionic/angular project to share data across components, but the service isn't working as I expected with objects.

In one component (i.e.: a parent), when a local object that got the attributes values from subscribing to the service has a value changed it propagates the value to other components, but I think this just could happen if the value is changed via the service with next().

Below a minimum example in a blank ionic project, to compare a string and object in this scenario. In the parent component there're two methods, one that change the local values (where occurs the problem with the object and not with the string) and the other that change values via service (works as expected).

May I be doing something wrong, behaviorSubject works this way with objects or is it an issue??

data.service.ts

import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class DataService {

  private strSrc = new BehaviorSubject('default');
  readonly strCurr = this.strSrc.asObservable();

  private objSrc = new BehaviorSubject({ name : 'Default' });
  readonly objCurr = this.objSrc.asObservable();

  constructor() { }

  chgStr(strNew) {
    this.strSrc.next(strNew);
  }

  chgObj(objNew) {
    this.objSrc.next(objNew);
  }
} 

child.component.ts

import { Component, OnInit } from '@angular/core';
import { DataService } from '../../service/data.service';

@Component({
  selector: 'app-child',
  template: `
  <p>child data String: {{ str }}</p>
  <p>child data Object: {{ obj.name }}</p>
  `
})

export class ChildComponent implements OnInit {
  str: string;
  obj: { name: string };

  constructor(private data: DataService) { }

  ngOnInit() {
    this.data.strCurr.subscribe(data => this.str = data);
    this.data.objCurr.subscribe(data => this.obj = data);
  }
}

home.page.ts

import { Component, OnInit } from '@angular/core';
import { DataService } from '../service/data.service';

@Component({
  selector: 'app-home',
  template: `<ion-content>
  <p>parent data String: {{ str }}</p>
  <p>parent data Object: {{ obj.name }}</p>
  <br/>
  <app-child></app-child>
    <ion-button (click)="chgLocal()">
      change parent variable
    </ion-button>
    <ion-button (click)="chgBehaviorSubject()" color="secondary">
      change behaviorSubject
    </ion-button>
  </ion-content>`,
})
export class HomePage implements OnInit {
  str: string;
  obj: { name: string };

  constructor(private data: DataService) { }

  ngOnInit() {
    this.data.strCurr.subscribe(data => this.str = data);
    this.data.objCurr.subscribe(data => this.obj = data);
  }

  chgLocal() {
    this.str = 'changed String';
    this.obj.name = 'changed Object';
    // --Output in HTML
    // parent data String: changed String
    // parent data Object: changed Object

    // child data String: default
    // child data Object: changed Object <--PROBLEM!

    // --Expected output in HTML
    // parent data String: changed String
    // parent data Object: changed Object

    // child data String: default
    // child data Object: Defaul
  }

  chgBehaviorSubject() {
    this.data.chgStr('behavior string');
    this.data.chgObj({ name: 'Behavior Object' });
  }
}
1

1 Answers

2
votes

The problem is because of the mutation of the object and is not exclusive to Behavior Subject. Objects are passed by reference and when you mutate the object in parent, it also changes the other places the same object is referenced to.

You could do

this.data.objCurr.subscribe(data => this.obj = {...data}); // Assuming there are no nested objects inside data

In case of nested objects, you could use JSON.parse(JSON.stringify(data)) though it is not very performant and something like lodash may be used for the deep cloning utility (or you could write a recursive function that clones nested objects too)