Home > other >  BehaviorSubject: mutating current value between subscriptions
BehaviorSubject: mutating current value between subscriptions

Time:11-14

Assume I have BehaviorSubject with User interface:

interface User {
  firstName: string;
  lastName: string;
}

let user: User = {
  firstName: 'Cuong',
  lastName: 'Le',
};

let bs = new BehaviorSubject<User>(user);

There are two subsciptions, sub1 tried to change the first name. Sub2 subscribes later and user object has first name changed also as the sub1 did change it before:

let sub1 = bs.subscribe((u) => {
  u.firstName = 'An';

  console.log(u);
});

let sub2 = bs.subscribe((u) => {
  console.log(u);
});

it's hard to debug when this case happens in big Angular application. How we make the value immutable when subscribing?

I am looking deep immutable solution to prevent code somewhere else to change the data instead of the shadow one

CodePudding user response:

The simplest thing you could do is make the fields readonly on the user interface. This will prevent someone from changing a field value as long as the type is known.

interface User {
  readonly firstName: string;
  readonly lastName: string;
}

let sub1 = bs.subscribe((u) => {
  u.firstName = 'An'; // ts: Cannot assign to 'firstName' because it is a read-only property.
});

If you need to go further, you can define a class with properties that only have getters. The difference here is that even if somebody would to access the value in an untyped way and try to change the field, there would be no effect.

class ImmutableUser  {
  get firstName(): string { return this._firstName; }
  get lastName(): string { return this._lastName; }
  constructor(private _firstName: string, private _lastName: string) { }
}
let bs = new BehaviorSubject<User>(new ImmutableUser('Cuong', 'Le'));
let sub1 = bs.subscribe((u) => { 
  // this wouldn't produce an error if u was User instead of ImmutableUser.
  (u as any).firstName = 'An'; // runtime error: Cannot set property which has only a getter.
});

The last thing you could do is have all of your subscribers subscribe to an intermediary that creates a new version of the object. This seems extreme, especially if you have a lot of subscribers. However, if this is a big project and you're not trying break things, it might work in a pinch.

let bs2 = bs.pipe(map(x => ({...x})));
let sub1 = bs2.subscribe((u) => {
  (u as any).firstName = 'An';
  console.log(u); // { firstName 'An', lastName: 'Le' }
});
let sub2 = bs2.subscribe((u) => {
  console.log(u);  // { firstName 'Cuong', lastName: 'Le' }
});

CodePudding user response:

You can use the utility type screenshot

Cannot assign to 'firstName' because it is a read-only property. (2540)

CodePudding user response:

This is a problem I've had to solve several times. As long as you are passing simple non-circular objects through the subject this solution should work.

The one requirement is that you utilize the lodash library, which supports a very fast deep copy (no reference copies). I've looked everywhere and I've yet to find one that is faster.

let bs = (new BehaviorSubject<User>(user)).pipe(    
     map((userData) => _.cloneDeep(userData)) 
);

I've used this on fairly large state objects and I've always been impressed by how quickly it is able to perform copies. The implication here is that any emission coming out of the subject is a copy of the original, so changes happen in isolation and will never be able to trail back to the source.

  • Related