简体   繁体   中英

Return observable of object from subscribe of an http request angular

I am creating kind of store on my angular service layer.

private attributes: Observable<Attribute[]>;
private attributes$: BehaviorSubject<Attribute[]>;

that is filled when the user demands the allAttributes() . Then subsequent request for all the attributes or single attribute ( getAttribute(id) ) returns data from the same store.

Here is my getAttribute()

getAttribute(id: number): Promise<Observable<Attribute>> {
    return new Promise((resolve) => {
        let _attributeObservable;
        const _attributes: Attribute[] = this.getAttributesState();
        let _attributeFound = false;
        for (const _attribute of _attributes) {
            if (_attribute.id === id) {
                _attributeFound = true;
                break;
            }
        }
        if (_attributeFound) {
            _attributeObservable = this.attributes.pipe(map((_attributeList: Attribute[]) => {
                return _attributeList.find(_attribute => _attribute.id === id);
            }));
            resolve(_attributeObservable);
        } else {
            return this.http.get(`${this.context}/attributeService/getAttribute/${id}`)
                .subscribe((_attributeInfo: Attribute) => {
                    const _allAttributes = this.getAttributesState();
                    _allAttributes.push(_attributeInfo);
                    // push object to store that was not found
                    this.attributes$.next(Object.assign([], _allAttributes));
                    _attributeObservable = this.attributes.pipe(map((_attributeList: Attribute[]) => {
                        return _attributeList.find(_attribute => _attribute.id === id);
                    }));
                    resolve(_attributeObservable);
                });
        }
    });
}

and

getAttributesState(): Attribute[] {
   return this.attributes$.getValue();
}

Now there are cases where some other users can add the attribute , so that attribute will not be in the store. So if requested attribute is not found then http request is made and saved to store.

But the problem is if attribute found then it works, but the else part is not working. what could be the issue? Is this code can be simplified, the better approach?

After some time of refactoring the code I think I understand what this code is intended to do.

As I understand it correctly you want to avoid a server call if an attribute is stored already. By looking for an Attribute for a given id inside your BehaviourSubject indicate a stored attribute. If you did not find an Attribute the code will trigger the http client to fetch an Attribute from the server.

The cleanup looks like this.

import { Component, OnInit } from '@angular/core';
import { Observable, BehaviorSubject, of } from 'rxjs';
import { map } from 'rxjs/operators';
import { Attribute } from '../attribute';
import { HttpClient } from '@angular/common/http';

@Component({
  selector: 'app-attribute',
  templateUrl: './attribute.component.html',
  styleUrls: ['./attribute.component.scss']
})
export class AttributeComponent implements OnInit {

  private attributesAsObservable: Observable<Attribute[]>;
  private attributes$: BehaviorSubject<Attribute[]>;
  private context = 'localhost:3000';

  constructor(private http: HttpClient) { }

  ngOnInit() {
    let attributes = [{id: 12, name: 'test'}] as Attribute[];
    this.attributes$ = new BehaviorSubject<Attribute[]>(attributes)
    this.attributesAsObservable = of(attributes)

    console.log("Find id: 12", this.getAttribute(12))
    console.log("Find id causes server call: 1", this.getAttribute(1))

  }

  getAttribute(id: number): Observable<Attribute> {
    let attributeFound = this.findFromStored(id);
    if (attributeFound) {
      return of(attributeFound)
    } else {
      return of(this.fetchAttributeFromServer(id))
    }
  }

  private findFromStored(id: number): Attribute {
    let attributes = this.attributes$.getValue();
    return attributes.find(attribute => attribute.id === id)
  }

  private fetchAttributeFromServer(id: number): Attribute {
    this.httpCall(id).subscribe( attribute => {
      this.addNewAttributeToStore(attribute);
    });
  }

  private addNewAttributeToStore(attribute: Attribute) {
    let attributes: Attribute[] = this.attributes$.getValue();
    attributes.push(attribute)
    this.attributes$.next(attributes)
  }

  //THIS SHOULD BE EXTRACTED TO A SERVICE
  private httpCall(id: number): Observable<Attribute> {
    console.log('Return fake http Observable');
    return of<Attribute>({id: 1, name: 'test'})
    // return this.http.get<Attribute>(
    //   `${this.context}/attributeService/getAttribute/${id}`
    // );
  }
}

This Refactoring does not work if you are fetching the value from the server. The reason is the async http call. The HTTP client will return on Observable and we cannot be sure when the server will respond.

IMO what you could do is to introduce a new property on your component. This property holds a BehaviourSubject<Attribute> (Or in your case BehaviourSubject<Observable<Attribute>> ). Lets call it currentAttribute$. Anytime you call getAttribute(id) you are going to call currentAttribute$.next() .

Lets change it.

import { Component, OnInit } from '@angular/core';
import { Observable, BehaviorSubject, of } from 'rxjs';
import { Attribute } from '../attribute';
import { HttpClient } from '@angular/common/http';

@Component({
  selector: 'app-attribute',
  templateUrl: './attribute.component.html',
  styleUrls: ['./attribute.component.scss']
})
export class AttributeComponent implements OnInit {

  private attributesAsObservable: Observable<Attribute[]>;
  private attributes$: BehaviorSubject<Attribute[]>;
  private currentAttributeFoundById: BehaviorSubject<Attribute>;
  private context = 'localhost:3000';

  constructor(private http: HttpClient) { }

  ngOnInit() {
    let attributes = [{id: 12, name: 'test'}] as Attribute[];
    this.attributes$ = new BehaviorSubject<Attribute[]>(attributes);
    this.attributesAsObservable = of(attributes);
    this.currentAttributeFoundById = new BehaviorSubject<Attribute>({});

    this.currentAttributeFoundById.subscribe(attribute => {
      console.log('Current Attribute by ID is:', attribute)
    });

    this.setAttributeBy(12);
    this.setAttributeBy(12);
    this.setAttributeBy(1);
  }

  setAttributeBy(id: number) {
    let attributeFound = this.findFromStored(id);
    if (attributeFound) {
      this.currentAttributeFoundById.next(attributeFound);
    } else {
      this.setAttributeFromServer(id)
    }
  }

  private findFromStored(id: number): Attribute {
    let attributes = this.attributes$.getValue();
    return attributes.find(attribute => attribute.id === id)
  }

  private setAttributeFromServer(id: number) {
    this.httpCall(id).subscribe(attribute => {
      this.addNewAttributeToStore(attribute);
      this.currentAttributeFoundById.next(attribute);
    });
  }

  private addNewAttributeToStore(attribute: Attribute) {
    let attributes: Attribute[] = this.attributes$.getValue();
    attributes.push(attribute)
    this.attributes$.next(attributes)
  }

  //THIS SHOULD BE EXTRACTED TO A SERVICE
  private httpCall(id: number): Observable<Attribute> {
    console.log('Return fake http Observable');
    return of<Attribute>({id: 1, name: 'test'})
    // return this.http.get<Attribute>(
    //   `${this.context}/attributeService/getAttribute/${id}`
    // );
  }
}

This change allows the code to behave like intended (only fetch from the server if needed).

As mentioned in the comments you can use switchMap , concatMap , mergeMap etc. to get the first solution to work.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM