Search code examples
angulartypescriptrxjsngrx

Angular 8: Why is my non-observable type getting updated whenever the data from which it was copied is updated?


Using Angular 8 and Ngrx 8(irrelevant but here for completeness), I have a state service that manages my view model state. In it, I have the viewModel$ property which is a behavior subject. When the service originally gets its data, the viewModel$.next() is called with the new view model value, and another property called originalValue is also set to that view model value. That works fine. What I want to happen, and what I thought I had coded, is for the original value to remain unchanged so that if my user wants to revert changes, I simply throw away the updated view model and set it back to the original value.

What actually happens is that whenever my view model is updated, the originalValue property is also (erroneously) getting updated. This is puzzling, since the originalValue property is not an observable. I'm guessing it has something to do with closures, but I'm not adept enough to puzzle it out.

Bottom line question-- why is this happening and how can I fix it? How can I achieve the functionality I want-- that is, to keep the old version of data so that I can revert it if necessary? (you may wonder why I don't just retrieve the data again from the store. I could/can do that and it would work but I'd like to understand what is going wrong here regardless. I have other parts of a "Project" that need to be reverted at the same time, and I don't want to incur the additional time/expense of converting state models to view models if I can avoid it.)

Here is the relevant code:

In the state service,

properties:

  public viewModel$: BehaviorSubject<ProjectCoreViewModel>;
  public viewModelLoaded$: BehaviorSubject<boolean> = new BehaviorSubject(false);
  public originalValue: ProjectCoreViewModel;
  public isPristine$: BehaviorSubject<boolean> = new BehaviorSubject(true);
  private isInitialized = false;

initial selection of data (through facade to retrieve from store)

  public selectData() {
    this.facade.getProjectCore()
      .subscribe(core => this.fromStoreStateModel(core));

  }

conversion from state model to view model, after which the updateViewModel$ method is called:

  fromStoreStateModel(projectCore: ProjectCore) {

    if (projectCore) {
      const viewModel: ProjectCoreViewModel = {
        projectId                 : projectCore.projectId,
        studioId                  : this.facade.studioId,
        ravelryProjectId          : projectCore.ravelryProjectId,
        ravelryLink               : projectCore.ravelryLink,
        isPrivate                 : false,
        title                     : projectCore.projectTitle,
        startedDate               : projectCore.startedDate,
        completedDate             : projectCore.completedDate,
        warpingMethod             : projectCore.warpingMethodId,
        weavingTechnique          : projectCore.weavingTechniqueId,
        reedId                    : projectCore.reedId,
        loomId                    : projectCore.loomId,
        itemCategory              : projectCore.itemCategoryId,
        itemQuantity              : projectCore.quantity,
        designName                : projectCore.designName,
        sourcePublicationId       : projectCore.sourcePublicationId,
        itemWidth                 : projectCore.itemWidth,
        itemLength                : projectCore.itemLength,
        widthHeightRatio          : this.getSizeRatio(projectCore),
        coreNotes                 : projectCore.coreNotes,
        sourcePublicationReference: projectCore.sourcePublicationReference,
      };
      this.updateViewModel$(viewModel);

    }
  }

updateViewModel$ method:

  updateViewModel$(value: ProjectCoreViewModel) {

    if (value) {

      if (!this.viewModel$) {
        if (!this.isInitialized) {
          this.setOriginalValue(value);
          this.viewModel$ = new BehaviorSubject(value);
          this.viewModel$.pipe(
            skip(1)).subscribe(model => {

            console.log('changed core view model', model); 
            console.log('original value in state service', this.originalValue); // this has changed too!

            this.isPristine$.next(false);
          });
          this.isInitialized = true;
        }
      } else {
        this.isPristine$.next(false);
        this.viewModel$.next(value);
      }
      this.setLoadedStatus(true);
    } else {
      this.setLoadedStatus(false);
    }
  }

setOriginalValue method:


  private setOriginalValue(viewModel: ProjectCoreViewModel) {
    this.originalValue = viewModel;
    this.isPristine$.next(true);
  }

In my component this piece updates the view model:

this.coreForm.valueChanges.pipe(debounceTime(1000))
      .subscribe(value => {

        model.title = value.name;
        model.startedDate = value.startedDate;
        model.completedDate = value.completedDate;
        model.itemCategory = value.itemType;
        model.weavingTechnique = value.weavingTechnique;
        model.warpingMethod = value.warpingMethod;
        model.reedId = value.reed;
        model.loomId = value.loom;
        model.itemQuantity = value.quantity;
        model.designName = value.designName;
        model.sourcePublicationId = value.designSource;
        model.sourcePublicationReference = value.designSourceReference;
        model.itemWidth = value.itemWidth;
        model.itemLength = value.itemLength;

        this.projectCoreViewModel$.next(model);

      });

Solution

  • It's not a closure, I guess you just share the same object in 2 places, and modifying it in one place, so in another place it will be updated as well.

    To fix that try to copy it when you saving it into this.originalValue, you can do that by switching this line:

    this.originalValue = viewModel;
    

    into this:

    this.originalValue = {...viewModel};
    

    Hope that helps.