Skip to content

Highcharts Error #18 and ghost DOM artifacts when component is destroyed before draw() runs #499

@kdagnan

Description

@kdagnan

Version: 7.0.0

Description

When an ember-highcharts component is inserted and then immediately destroyed before draw() has a chance to run: Highcharts Error #18 — fires from Highcharts' internal animation/event callbacks after the container element is removed

Root cause

onDidInsert is async. It awaits _importHighchartsDeps(), then calls drawAfterRender(), which schedules draw() via scheduleOnce('afterRender', ...).

If the component is destroyed during that async gap (or before the afterRender callback fires), willDestroy runs while this.chart is still null, so this.chart?.destroy() is a no-op. Then draw() fires from the afterRender queue, creates a real Highcharts chart instance, and assigns it to this.chart on a component that is already dead. Nothing ever calls chart.destroy(), the SVG nodes are orphaned, and when the container is torn from the DOM, any pending animation callbacks fire against a destroyed chart → Error #18.

Minimal reproduction

{{! some-parent.hbs }}
{{#if this.showChart}}
  <HighCharts @content={{this.series}} @chartOptions={{this.options}} />
{{/if}}
// some-parent.js
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';

export default class SomeParent extends Component {
  @tracked showChart = true;

  constructor(owner, args) {
    super(owner, args);
    // Flip showChart off immediately after insertion — simulates a reactive
    // value (e.g. an error flag in a service) being reset during navigation.
    Promise.resolve().then(() => {
      this.showChart = false;
    });
  }
}

With the reproduction above, onDidInsert begins, awaits module imports, then scheduleOnce('afterRender', this, this.draw) is registered. By the time afterRender fires, the component has been destroyed. draw() creates the chart anyway, chart.destroy() is never called, DOM nodes linger and Error #18 follows.

Suggested fix

Guard draw() against being called after the component is destroyed:

draw() {
  if (this.isDestroying || this.isDestroyed) {
    return;
  }
  const element = this.el?.querySelector('.chart-container');
  // ...rest of existing logic
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions