2
votes

BehaviorSubject envoie la même référence d'état à tous les abonnés

Dans notre application à page unique, nous avons développé une classe de magasin centralisée qui utilise un comportement RxJS soumis à la gestion de l'état de notre application et de toute sa mutation. Plusieurs composants de notre application s'abonnent au sujet de comportement de notre boutique afin de recevoir toute mise à jour de l'état actuel de l'application. Cet état est ensuite lié à l'interface utilisateur afin que chaque fois que l'état change, l'interface utilisateur reflète ces modifications. Chaque fois qu'un composant souhaite modifier une partie de l'état, nous appelons une fonction exposée par notre magasin qui effectue le travail requis et met à jour l'état appelant ensuite sur le sujet de comportement. Jusqu'à présent, rien de spécial. (Nous utilisons Aurelia comme un framework qui effectue une liaison bidirectionnelle)

Le problème auquel nous sommes confrontés est que dès qu'un composant change sa variable d'état local qu'il reçoit du magasin, les autres composants sont mis à jour même si next () n'a pas été appelé sur le sujet lui-même.

Nous avons également essayé de nous abonner sur une version observable du sujet car observable est censé envoyer une copie différente des données à tous les abonnés, mais il semble que ce n'est pas le cas .

Il semble que tous les abonnés au sujet reçoivent une référence de l'objet stocké dans le sujet du comportement.

observable.subscribe((val) => {
  stateFromObservable = JSON.parse(JSON.stringify(val));
});

J'ai fait un stackblitz avec le code au dessus de. https://stackblitz.com/edit/rxjs-bhkd5n

La seule solution de contournement que nous avons jusqu'à présent est de cloner l'état dans certains de nos abonnés où nous prenons en charge l'édition via la liaison comme suit:

import { BehaviorSubject, of } from 'rxjs'; 

const initialState = {
  data: {
    id: 1, 
    description: 'initial'
  }
}

const subject = new BehaviorSubject(initialState);
const observable = subject.asObservable();
let stateFromSubject; //Result after subscription to subject
let stateFromObservable; //Result after subscription to observable

subject.subscribe((val) => {
  console.log(`**Received ${val.data.id} from subject`);
  stateFromSubject = val;
});

observable.subscribe((val) => {
  console.log(`**Received ${val.data.id} from observable`);
  stateFromObservable = val;
});

stateFromSubject.data.id = 2;
// Both stateFromObservable and subject.getValue() now have a id of 2.
// next() wasn't called on the subject but its state got changed anyway

stateFromObservable.data.id = 3;
// Since observable aren't bi-directional I thought this would be a possible solution but same applies and all variable now shows 3

Mais cela ressemble plus à un hack qu'à un vraie solution. Il doit y avoir un meilleur moyen ...


3 commentaires

Vous utilisez un objet imbriqué comme objet d'état. Cela va être un casse-tête à gérer, car il faut reconstruire l'objet à chaque fois que l'état est changé. Sur la base de la question, il ne semble pas que vous compreniez clairement comment un magasin d'État devrait fonctionner.


Pour ajouter à un commentaire ci-dessus, vous ne devriez jamais avoir besoin de JSON.parse (JSON.stringify car il crée une copie profonde qui est très inefficace. Les frameworks de gestion d'état utilisent une copie superficielle ( ... ou Object.assign ), mais comme commenté ci-dessus, vous devez être très prudent avec les objets imbriqués, ou éviter de les avoir.


@ JuliusDzidzevičius Je sais que dans ce cas particulier, la copie complète complète est excessive mais elle est là pour simplifier le code. Dans notre code, nous avons une fonction de clonage qui est un peu plus intelligente. Merci pour le conseil.


3 Réponses :


1
votes

La seule solution de contournement dont nous disposons jusqu'à présent est de cloner l'état de certains de nos abonnés où nous prenons en charge l'édition via une liaison comme suit:

Je ne pense pas pouvoir répondre à cela sans réécrire votre boutique.

stateFromSubject.data.id = 2;

Cet objet d'état contient des données profondément structurées. Chaque fois que vous devez muter l'état, l'objet doit être reconstruit.

Alternativement ,

const subject = new BehaviorSubject(initialState);

function getStore(): Observable<any> {
   return subject.pipe(
      map(obj => Object.freeze(obj))
   );
}

function getById(id: number): Observable<any> {
   return getStore().pipe(
      map(state => state[id]),
      distinctUntilChanged()
   );
}

function getIds(): Observable<number[]> {
   return getStore().pipe(
      map(state => state._index),
      distinctUntilChanged()
   );
}

C'est à peu près aussi profond d'un objet d'état que je créerais. Utilisez une paire clé / valeur pour mapper entre les ID et les valeurs d'objet. Vous pouvez désormais écrire facilement des sélecteurs.

function append(data: Object) {
    const state = subject.value;
    subject.next({...state, [data.id]: Object.freeze(data), _index: [...state._index, data.id]});
}

function remove(id: number) {
    const state = {...subject.value};
    delete state[id];
    subject.next({...state, _index: state._index.filter(x => x !== id)});
}

Lorsque vous souhaitez modifier un objet de données. Vous devez reconstruire l'état et également définir les données.

function getById(id: number): Observable<any> {
   return subject.pipe(
       map(state => state[id]),
       distinctUntilChanged()
   );
}

function getIds(): Observable<number[]> {
   return subject.pipe(
      map(state => state._index),
      distinctUntilChanged()
   );
}

Une fois que vous avez fait cela. Vous devez geler les consommateurs en aval de votre objet d'état.

const initialState = {
   1: {id: 1, description: 'initial'},
   2: {id: 2, description: 'initial'},
   3: {id: 3, description: 'initial'},
   _index: [1, 2, 3]
};

Plus tard, lorsque vous faites quelque chose comme ceci:

const initialState = {
  data: {
    id: 1, 
    description: 'initial'
  }
}

Vous obtiendrez une erreur d'exécution. p>

Pour info: ce qui précède est écrit en TypeScript


0 commentaires

3
votes

Oui, tous les abonnés reçoivent la même instance de l'objet dans le sujet de comportement, c'est ainsi que fonctionnent les sujets de comportement. Si vous voulez faire muter les objets dont vous avez besoin pour les cloner.

J'utilise cette fonction pour cloner mes objets, je vais les lier aux formes angulaires

data$ | clone as data

Donc si vous avez une donnée observable $ vous pouvez créer un clone observable $ où les abonnés à cet observable obtiennent un clone qui peut être muté sans affecter les autres composants.

clone$ = data$.pipe(map(data => clone(data)));

Donc, des composants qui sont juste l'affichage des données peut s'abonner aux données $ pour plus d'efficacité et celles qui muteront les données peuvent s'abonner au clone $.

Lisez ma bibliothèque pour Angular https://github.com/adriandavidbrand/ngx-rxcache et mon article à ce sujet https://medium.com/@adrianbrand/angular-state-management-with-rxcache-468a865fc3fb il entre dans le besoin de cloner des objets afin de ne pas muter les données que nous lions aux formulaires.

Cela soun ds comme les objectifs de votre magasin sont les mêmes que ma bibliothèque de gestion d'état angulaire. Cela pourrait vous donner quelques idées.

Je ne suis pas familier avec Aurelia ou si elle a des tuyaux mais cette fonction de clonage est disponible dans le magasin avec exposer mes données avec un clone $ observable et dans les templates avec un clone pipe qui peut être utilisé comme

const clone = obj =>
  Array.isArray(obj)
    ? obj.map(item => clone(item))
    : obj instanceof Date
    ? new Date(obj.getTime())
    : obj && typeof obj === 'object'
    ? Object.getOwnPropertyNames(obj).reduce((o, prop) => {
        o[prop] = clone(obj[prop]);
        return o;
      }, {})
    : obj;

L'important est de savoir quand cloner et non cloner. Vous n'avez besoin de cloner que si les données vont subir une mutation. Il serait vraiment inefficace de cloner un tableau de données qui ne sera affiché que dans une grille.


0 commentaires

0
votes

Le gros problème logique avec votre exemple est que l'objet transmis par le sujet est en fait une référence d'objet unique. RxJS ne fait rien hors de la boîte pour créer des clones pour vous, et c'est bien sinon cela entraînerait des opérations inutiles par défaut si elles ne sont pas nécessaires.

Ainsi, bien que vous puissiez cloner la valeur reçue par les abonnés, vous n'êtes toujours pas enregistré pour l'accès à BehaviorSubject.getValue (), qui renverrait la référence d'origine. De plus, avoir les mêmes références pour certaines parties de votre état est en fait bénéfique à bien des égards, car les tableaux par exemple peuvent être réutilisés pour plusieurs composants d'affichage au lieu de devoir les reconstruire à partir de zéro.

Ce que vous voulez faire à la place, c'est tirer parti d'un modèle de source de vérité unique, similaire à Redux, où au lieu de vous assurer que les abonnés obtiennent des clones, vous traitez votre état comme un objet immuable. Cela signifie que chaque modification entraîne un nouvel état. Cela signifie en outre que vous devez limiter les modifications aux actions (actions + réducteurs dans Redux) qui construisent un nouvel état du courant plus les modifications nécessaires et retournent la nouvelle copie.

Maintenant, tout cela peut sembler beaucoup de travail, mais vous devriez jeter un œil à la Aurelia Store Plugin , qui partage à peu près le même concept que vous et s'assure que les meilleures idées de Redux sont apportées au monde d'Aurelia.


0 commentaires