简体   繁体   中英

Angular HttpInterceptor Cache Not Triggering Change Detection

Updated with code to clarify. The TVC Component hosts a Trading View lightweight-charts component.

There is a side nav with a list of items. And every time a new/different item is selected, it fires this.data.getDataForSymbol() in the Main Content Component. The chart re-renders perfectly when NOT using the caching... but when the cache is used (and confirmed to be working)... the graph does not re-render.

Here is the component that renders the chart:

@Component({
  selector: 'tvc',
  template: '<div #chart></div>',
})
export class TvcComponent implements AfterViewInit {

  @ViewChild('chart') chartElem: ElementRef;

  @Input()
  data: (BarData | WhitespaceData)[] | null;

  chart: IChartApi = null;

  ngAfterViewInit() {
    this.buildChart();
  }

  buildChart() {
    this.chart = createChart(<HTMLElement>this.chartElem.nativeElement, {
      width: 600,
      height: 300,
      crosshair: {
        mode: CrosshairMode.Normal,
      },
    });

    this.chart.timeScale().fitContent();
    const candleSeries = this.chart.addCandlestickSeries();
    candleSeries.setData(this.data);
  }
}

And here is the component that hosts the TvcComponent, providing data to the chart:

@Component({
  selector: 'main-content',
  template: `
      <div *ngIf="monthly$ | async as monthly">
        <tvc
          [data]="monthly"
        ></tvc>
      </div>`
})
export class MainContentComponent implements OnInit {

  monthly$: Observable<any[]>;

  constructor(
    private route: ActivatedRoute,
    private itemStore: ItemStore,
    private data: DataService
  ) {}

  ngOnInit(): void {
    this.route.params.subscribe((params) => {
      let id = params['id'];
      this.itemStore.items$.subscribe((items) => {
        this.monthly$ = this.data.getDataForSymbol(id, 'monthly');
      });
    });
  }
}

Here is the relevant code for the interceptor service:

@Injectable({ providedIn: 'root' })
export class CacheInterceptor implements HttpInterceptor {
  constructor(private cache: HttpCacheService) {}

  intercept(
    req: HttpRequest<any>,
    next: HttpHandler
  ): Observable<HttpEvent<any>> {
    const cachedResponse = this.cache.get(req.urlWithParams);

    if (cachedResponse) {
      console.log(`${req.urlWithParams}: cached response`);
      return of(cachedResponse);
    }
    return next.handle(req).pipe(
      tap((event) => {
        if (event instanceof HttpResponse) {
          this.cache.put(req.urlWithParams, event);
          console.log(`${req.urlWithParams}: response from server`);
        }
      })
    );
  }
}

and the caching service:

@Injectable()
export class HttpCacheService {
  private cache = {};

  get(url: string): HttpResponse<any> {
    return this.cache[url];
  }
  put(url: string, resp: HttpResponse<any>): void {
    this.cache[url] = resp;
  }
}

I've implemented an HttpInterceptor for caching (example from Angular Github), and am caching the HttpResponse for data that is then subscribed to with an async pipe in the template - and passed as an input property to a child component. The observable contains data that renders a chart.

The data is (largely) static and selecting different items triggers a new Http Request. So if someone bounces back-and-forth between several charts, they will be making multiple (repeat) calls unnecessarily. Hence, the caching.

The problem is that while the cache works beautifully as determined via console logging)... the graph does not update/re-render when accessing data from the cache. The first time you select Item A, it gets the data from the server and renders correctly. If you move select Item B (not in cache), it makes the server request, puts the response in the cache, and renders the correct graph. The problem is if you switch BACK to Item A, it gets the correct data from the cache, but does NOT update the graph.

I am using default Change Detection.

I assume that the monthly$: Observable<any[]> variable gets changed correctly from what you wrote and that the Observable gets the new value (you can check that by logging). If that is the case, then the [data]="monthly" binding will be correctly updated by the change-detection.

That would mean, your problem is that the tvc component does not update correctly, because it does not react to changes in @Input() data . If you change the component to the following, it should work:

(There might be syntax errors on my part because I write this without being able to check it because you did not provide a working example code)

@Component({
  selector: 'tvc',
  template: '<div #chart></div>',
})
export class TvcComponent implements AfterViewInit {

  @ViewChild('chart') chartElem: ElementRef;
  
  private _data: (BarData | WhitespaceData)[] | null;

  get data(): (BarData | WhitespaceData)[] | null {
     return this._data;
  }

  @Input()
  set data(value: (BarData | WhitespaceData)[] | null) {
     // this gets called by the change-detection when the value of monthly changes from the [data]="monthly" binding
     // with that, we can use it to refresh the data
     // because the data is not bound to the chart by angular through a template, we have to do it manually. the change-detection only goes so far
     this._data = value;
     this.refreshData(); 
  }

  chart: IChartApi = null;
  candleSeries: any = null; // I don't know the correct type so I use any. You should change that

  ngAfterViewInit() {
    this.buildChart();
    this.refreshData();
  }

  buildChart() {
    this.chart = createChart(<HTMLElement>this.chartElem.nativeElement, {
      width: 600,
      height: 300,
      crosshair: {
        mode: CrosshairMode.Normal,
      },
    });

    this.chart.timeScale().fitContent();
    this.candleSeries = this.chart.addCandlestickSeries();
  }

  refreshData() {
    if (!this.candleSeries) return; // might not be initialized yet
    // I don't know the library, so I can't be sure that this is enough to update the data. 
    // You may have to do more. You can put a log here to see if it triggers
    this.candleSeries.setData(this.data);
  }
}

I hope this works for you. Just make sure that the data setter gets called correctly when you change the data. The rest can then be handled in the refreshData() method

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