Search code examples
javascriptangularrxjsangular-servicesangular-components

What is the proper way to share data between two components using rxjs ReplaySubject?


I've developed a component with two views. Component A has a contact form, and Component B is the "Thank you" page.

Component A: You fill the form and submit it. As soon as the response arrives, a new ReplaySubject value is created. The user will be routed to the component B.

Component B: The component is initialized. The component gets the value from the subject. The view is rendered and displays the thank you message.

HTTP Call response (Returned after a successful Form data post request):

{
   "status": 200,
   "body": {
              "message": "We have received your request with the card information below and it will take 48h to be processed. Thank you!",
              "card": {
                    "name": "John Doe",
                    "email": "[email protected]",
                    "accountNumber": "12345-67890"
              }
            },
   "type": "ItemCreated"
}

Component A (Form) code:

import { Component } from '@angular/core';
import { FormBuilder, Validators } from '@angular/forms';
import { RequestCardWidgetService } from './request-card-widget.service';
import { RouterService } from '@framework/foundation/core';
import { Item } from '@pt/request-card-data'

@Component({
  selector: 'pt-request-card-form',
  templateUrl: './request-card-form.template.html',
  providers: [RouterService]
})
export class RequestCardFormComponent {
  constructor(private fb: FormBuilder, private data: RequestCardWidgetService, private router: RouterService){}

  item: Item = {
    name: '',
    email: '',
    accountNumber: ''
  };

  requestCardForm = this.fb.group({
    name: ['', Validators.required],
    email: ['', Validators.email],
    accountNumber: ['', Validators.required]
  })

  onSubmit() {
    this.item = this.requestCardForm.value;
    this.data.requestCard(this.item)
      .subscribe(data => {
        this.data.processResult(data);
        this.router.navigate(['/success']);
      });
  }

}

Component B (Thank you page) code:

import { Component } from '@angular/core';
import { RequestCardWidgetService } from './request-card-widget.service';

@Component({
  selector: 'pt-request-card-success',
  templateUrl: './request-card-success.template.html'
})
export class RequestCardSuccessComponent {
  messages: any; // TODO: To use the proper type...

  constructor( private requestCardService: RequestCardWidgetService) {
    this.messages = this.requestCardService.message;
  }
}

Component B Template (Thank you page):

<div *ngIf='(messages | async) as msg'>
  {{ msg.message}}
</div>

Component Service code:

import { Injectable } from '@angular/core';
import { HttpResponse } from '@angular/common/http';

import { Observable, ReplaySubject } from 'rxjs';
import { map, take } from 'rxjs/operators';

import {
  RequestCardDataService,
  Item,
  ItemCreated
} from '@example/request-card-data';

@Injectable()
export class RequestCardWidgetService {

  constructor(private dataService: RequestCardDataService) { }

  private readonly results = new ReplaySubject<ItemCreated>();

  readonly message: Observable<ItemCreated> = this.results; // Message Line. This is the variable that I'm rendering in the template. Is this the correct way of extracting subject values?

  requestCard (card: Item): Observable<ItemCreated> {
    return this.dataService.postCardRecord(card).pipe(
      take(1),
      map((response: HttpResponse<ItemCreated>): ItemCreated | {} => {
        return response.body
          ? response.body
          : {};
      })
    );
  }

  processResult(data: ItemCreated) {
    this.results.next(data);
  }

}

Recap: Component A has a form. After you submit the form, the results are stored as a new value in the subject. The user is routed to the thank you page. The thank you page component renders the element and gets the newest value from the subject. Then it renders the contents.

This code works, but I do have some questions.

Question: Is this the proper way of using the Subject?

Is this:

readonly message: Observable<ItemCreated> = this.results;

the proper way of extracting the values from a subject? (I'm passing 'message' to the view.)

Are there better ways to achieve the same result? Thanks a lot in advance.


Solution

  • Is this the right way to use a subject?

    ReplaySubect unconstrained will replay all of the values it has previously emitted to new subscribers. This could lead to situations where a user could receive previously emitted messages until they finally receive the current message. Therefore, either constrain the subject or consider using a BehaviorSubject instead.

    Extracting values from the subject

    A Subject and all of its derivatives are both Observables and Observers. When providing a subject to a consumer, you do not want to expose the Observer interface, i.e., a consumer should never be able to call next, error or complete. Thus, as suggested in a comment, you should ensure you are only exposing the Observable interface to consumers by first calling the asObservable method.

    readonly message: Observable<ItemCreated> = this.results.asObservable();

    Next Steps

    If you want to continue using service-based communication between components, then I think you have opportunities to clean/refine your code per the docs linked in the question comments.

    If your application is going to grow in complexity, I would steer you down the Redux-style architecture and look into NgRx and specifically, the use of effects to manage side effects.

    Effects can meet all of your requirements with simple, discreet observable constructs, i.e., an effect to handle the form submission, receive the response, even navigate to the success page. More information about effects can be found here.

    A redux architecture can be overkill for simple tasks, but if you're working on a large app managing a large state tree, I prefer this approach over service-based integrations.