Angular 7: Service instance working incorrectly

Viewed 189

I have two components in my app - 'filter' and 'home'. These two components are child components of app component.

I have a below object of which type I would like to pass from filter component to home component via a service. And as I want this data to updated in home component whenever it changes in filter component, I use BehaviorSubject in service.

Model: filter-response.model.ts

export interface FilterResponse{
    filter:string;
    filterlist:string[];
}

filter.component.ts

export class FilterComponent implements OnInit {
    filterMainList:FilterResponse[]=[
        {
            "filter": "abc",
            "filterlist": ["a","b","c"]
        },
        {
            "filter": "def",
            "filterlist":["d","e","f"]
        },
        {
            "filter": "ghi",
            "filterlist": ["g","h","i"]
        }
    ];
    constructor(private filterService:FilterService,) {
        this.filterService.setFilterConditions(this.filterMainList);
    }
    ngOnInit() {
    }
}

In home component I receive the data and I would like to make some changes to it.

home.component.ts

export class HomeComponent implements OnInit {
  constructor(private filterService:FilterService) {
    this.filterService.getFilterConditions().subscribe((data)=>{
      let tc:FilterResponse[]=data
      for(let f in tc){
        tc[f].filterlist=['changes'];//this is where I make the changes
      }
    });
   }
  ngOnInit() {
  }
}

I have a filter service which has the behavior subject. This is where I got confused. I log the data here in service before next(), And the data I actually get here is which I change in home component, which means the changes I make with the instance of data in home component is also affecting(changing) the instance of data in service.

filter.service.ts

@Injectable({
    providedIn:'root'
})
export class FilterService{
    private filterConditions = new BehaviorSubject<FilterResponse[]>(null);
    constructor(){}
    setFilterConditions(filterList:FilterResponse[]){
        console.log(filterList);//data changes incorrectly
        this.filterConditions.next(filterList);
    }
    getFilterConditions():Observable<FilterResponse[]>{
        return this.filterConditions.asObservable();        
    }
}

My console log:

console log

The data that should actually be the one I just passed in the filter component. But it displays the data that I modify in home component.

And finally app.component.ts, if it could help

app.component.ts

<app-filter></app-filter>
<app-home></app-home>

Another thing I would like to bring to your notice is. I tried to recreate the scenario in stackblitz. But the twist is it working fine there.

https://stackblitz.com/edit/angular-ivy-9th5pj

Kindly explain what I am missing in my project. Or is this the actual way the service will work in angular.

3 Answers

Make this change in your service file

 setFilterConditions(filterList:FilterResponse[]){
    this.filterConditions.next(JSON.parse(JSON.stringify(filterList)));
}

This will clone you array. There are many others ways also to clone a object/array in JS Different ways to Clone object in JS

Reason:

In TS, when passing an object as function parameter, you are passing a reference to that object. So when modifying an object that was passed into a function you are modifying that original object.

Similarly the array/object which are received in subscribed function of HomeComponent are received by reference. So value change in HomeComponent is affected in service as well.

I misunderstood your problem: I thought that your question was why is the console.log output showing a modified array while being placed before the modification.

Like explained in khush's answer, non primitive types are passed by reference: JS does not pass a copy of the array to functions/methods, but the array itself. So that's the same array instance (filterMainList) that is passed with next and that is modified in your HomeComponent.

Now, you have console.log(filterList) before the modification of the data, but the log output still shows the changes. This is because when you click on the Array(3) in your browser's console, the debugger evaluates the value of the variable at this time , which has already been modified by then.

This is not visible on your stackblitz example because you don't log the array, but JSON.stringify(arr). In this case, you don't need to expand the value in your console, the value is already displayed as a string and won't be re-evaluated. If we just log the variable, the stackblitz demo will reproduce the same 'problem'

If this behaviour is not the desired one, you can clone the data passed like khush's suggested; either from the component sending it, or directly in your service before calling next, which you'll only have to do once if you have multiple calls in your code to setFilterConditions

I will just extend Khush's answer. So, if nothing works for creating a deep copy then you can go for cloneDeep function from loadash. Please use this option in worst case (hotfix issue). This issue occur due to shallow copy. So, when you are returning the array in each case you are not returning a new array but the address of the array. So whetever change will occur to the array will occur to your original array. And when you use JSON.stringigy or deepClone function, internally you create copy of array with different address

setFilterConditions(filterList:FilterResponse[]){
    this.filterConditions.next(cloneDeep(filterList));
}
Related