Integrating Third-Party Libraries like Highcharts into a PCF Control

A technical guide to loading Highcharts inside a Power Apps Component Framework (PCF) control: webpack externals versus npm bundling, the CDN loading pattern, and a component lifecycle that does not leak memory.

Need to build a rich chart or use a specialized JavaScript library in a Power Apps custom control? This guide demonstrates how to integrate third-party libraries like Highcharts into a Power Apps Component Framework (PCF) control. We cover configuring webpack externals, loading dependencies via CDN or npm, and managing the component lifecycle to avoid memory leaks, providing a complete, working example.

This walkthrough uses Highcharts as the example, but the same pattern applies to any third-party JavaScript library you want inside a model-driven app: D3, Chart.js, a mapping SDK, or a rich text editor. Custom PowerApps Component Framework development is a core part of what Solzet builds for Dynamics 365 Customer Engagement clients, and the approach below is the one we use in production.

If you are still deciding who should write this code, our decision guide on hiring PCF developers: a dedicated team versus Upwork or Toptal freelancers compares the two routes and lists the questions that screen either one. If the control you are building has to run in a canvas app rather than a model-driven form, start with using PCF controls in canvas apps, which covers the environment setting and the API differences that apply there.

Two ways to load Highcharts: bundle or externalize

There are two supported ways to get Highcharts into a PCF control. Both are valid. The right one depends on whether you want a self-contained control or the smallest possible bundle.

Bundle it with npm (the default, simplest path)

Run npm install highcharts, import it, and let the PCF build pipeline (pcf-scripts, which wraps webpack) bundle Highcharts into your control bundle.js. Nothing extra to configure. The control is fully self-contained and works even where outbound CDN calls are blocked. The tradeoff is a larger bundle, since the whole library ships inside your control.

Externalize it and load from a CDN (smaller bundle)

Mark highcharts as a webpack external so the import resolves to a global Highcharts object at runtime instead of being bundled, then load the library from a CDN yourself. This keeps your control bundle small and lets multiple controls share one cached copy. The tradeoff is an external runtime dependency: the CDN must be reachable and allowed by the environment content security policy, and your code must wait for the script to load.

Step by step

Follow these steps to scaffold the control, add Highcharts, wire up the loading strategy, and manage the lifecycle. Step four is only needed if you externalize the library.

  1. Scaffold the PCF control

    Create the control project with the Power Platform CLI. Run pac pcf init with a namespace, a control name, and the field template, then run npm install. This gives you the standard PCF structure: ControlManifest.Input.xml, an index.ts that implements ComponentFramework.StandardControl, and the pcf-scripts build pipeline.

    Terminal
    pac pcf init --namespace Solzet --name HighchartsChart --template field
    npm install
  2. Add Highcharts as a dependency

    Install Highcharts from npm. Highcharts ships its own TypeScript definitions, so no separate types package is needed. Import it in index.ts as a namespace so the typed Options and Chart interfaces are available to you.

    Terminal
    npm install highcharts
    index.ts
    import * as Highcharts from 'highcharts';
  3. Decide how to load it: bundle or externalize

    By default webpack bundles Highcharts into your control. If you prefer to load it from a CDN to keep the bundle small, enable custom webpack in featureconfig.json and add a webpack.config.js that marks highcharts as an external. That tells webpack the import should resolve to the global Highcharts object at runtime rather than being bundled.

    featureconfig.json
    {
      "pcfAllowCustomWebpack": "on"
    }
    webpack.config.js
    // Merged into the PCF webpack build when custom webpack is enabled.
    // "highcharts" is now expected as a global at runtime, not bundled.
    module.exports = {
      externals: {
        highcharts: 'Highcharts'
      }
    };
  4. Load the CDN script when externalizing

    When Highcharts is external, the global must exist before your code uses it. Inject the CDN script once during init and resolve a promise when it loads. If you bundled Highcharts instead, skip this step, because the library is already inside your control.

    index.ts (only needed for the externalized / CDN path)
    private loadHighcharts(): Promise<void> {
      if ((window as unknown as { Highcharts?: unknown }).Highcharts) {
        return Promise.resolve();
      }
      return new Promise((resolve, reject) => {
        const script = document.createElement('script');
        script.src = 'https://code.highcharts.com/highcharts.js';
        script.onload = () => resolve();
        script.onerror = () => reject(new Error('Highcharts failed to load'));
        document.head.appendChild(script);
      });
    }
  5. Render the chart in updateView, create once and update in place

    Build the chart options from your bound parameters. In updateView, if no chart instance exists yet, create it with Highcharts.chart against your container element; otherwise call chart.update with the new options. Recreating the chart on every updateView call is the most common cause of flicker and leaked DOM nodes.

    index.ts
    public updateView(context: ComponentFramework.Context<IInputs>): void {
      const options: Highcharts.Options = {
        chart: { type: 'column' },
        title: { text: context.parameters.chartTitle.raw ?? '' },
        series: [{ type: 'column', data: this.readData(context) }]
      };
    
      if (!this.chart) {
        // Create once against the container the platform gave us in init.
        this.chart = Highcharts.chart(this.container, options);
      } else {
        // Update in place on every later render: no flicker, no leaked DOM.
        this.chart.update(options, true, true);
      }
    }
  6. Destroy the chart to avoid memory leaks

    The platform calls destroy when the control is removed. Highcharts attaches window resize listeners and SVG DOM, so you must call chart.destroy there and drop the reference. Without it, navigating between records or re-rendering a grid leaks a chart instance and its listeners every time.

    index.ts
    public destroy(): void {
      // Releases Highcharts SVG, event handlers, and window resize listeners.
      this.chart?.destroy();
      this.chart = undefined;
    }
  7. Build, test, and package

    Run npm run build to compile and bundle the control, then npm start watch to test it in the local PCF harness with sample data. When it works, package it into a solution with the pac solution commands and import it to test inside a real model-driven app.

    Terminal
    npm run build
    npm start watch

The complete control

Here is the full index.ts for a working control that renders a Highcharts column chart from bound parameters, updates in place, and cleans up on destroy. This assumes Highcharts is available (bundled or loaded from the CDN). Bind chartTitle, seriesName, and chartValue in ControlManifest.Input.xml, and map your real data inside readData.

index.ts
import * as Highcharts from 'highcharts';
import { IInputs, IOutputs } from './generated/ManifestTypes';

export class HighchartsChart
  implements ComponentFramework.StandardControl<IInputs, IOutputs> {
  private container: HTMLDivElement;
  private chart: Highcharts.Chart | undefined;

  public init(
    context: ComponentFramework.Context<IInputs>,
    notifyOutputChanged: () => void,
    state: ComponentFramework.Dictionary,
    container: HTMLDivElement
  ): void {
    // Keep the container; do not build the chart yet. updateView runs next
    // and again on every data change, so the chart is created there.
    this.container = container;
  }

  public updateView(context: ComponentFramework.Context<IInputs>): void {
    const options: Highcharts.Options = {
      chart: { type: 'column' },
      title: { text: context.parameters.chartTitle.raw ?? '' },
      credits: { enabled: false },
      series: [
        {
          type: 'column',
          name: context.parameters.seriesName.raw ?? 'Value',
          data: this.readData(context)
        }
      ]
    };

    if (!this.chart) {
      this.chart = Highcharts.chart(this.container, options);
    } else {
      this.chart.update(options, true, true);
    }
  }

  private readData(context: ComponentFramework.Context<IInputs>): number[] {
    // Map your bound field or dataset rows to a numeric series here.
    return [context.parameters.chartValue.raw ?? 0];
  }

  public getOutputs(): IOutputs {
    return {};
  }

  public destroy(): void {
    // Critical: without this, every re-render leaks a chart and its listeners.
    this.chart?.destroy();
    this.chart = undefined;
  }
}

Common pitfalls

Most problems with a third-party library inside PCF come down to the lifecycle. These are the ones we correct most often when reviewing controls.

Rebuilding the chart on every updateView

updateView is called frequently, including on resize and container changes. Calling Highcharts.chart again each time stacks up detached charts and their listeners. Create once, then update the existing instance.

Forgetting destroy

A PCF control is mounted and unmounted as users move between records, forms, and views. If destroy does not call chart.destroy, each visit leaves a live chart in memory. Over a long session this shows up as a slow, growing tab.

Assuming the CDN is always reachable

The externalized path depends on the CDN being allowed by the environment content security policy and reachable from the client. In locked-down tenants it may not be. When in doubt, bundle the library so the control is self-contained.

Sizing the chart before the container has a size

Highcharts reads the container dimensions when it renders. In some PCF hosts the container has no height until layout settles. Give the container an explicit height, or reflow the chart once the element is measured, so the chart is not created at zero size.

Frequently Asked Questions

Do I need to eject or patch webpack to use Highcharts in a PCF control?

No. For most cases you simply run npm install highcharts, import it, and the standard PCF build (pcf-scripts, which wraps webpack) bundles it into your control automatically. You only need a custom webpack configuration if you want to externalize the library and load it from a CDN. In that case you enable it in featureconfig.json with pcfAllowCustomWebpack set to on and add a webpack.config.js that lists highcharts under externals.

Should I bundle Highcharts or load it from a CDN?

Bundling is the simplest and most portable choice: the control is self-contained and works even where outbound CDN calls are blocked, at the cost of a larger bundle. Externalizing and loading from a CDN keeps the control bundle small and lets multiple controls share one cached copy, at the cost of an external runtime dependency that the environment content security policy must allow. If you are unsure, bundle it; move to a CDN only when bundle size is a real concern.

How do I stop a PCF chart control from leaking memory?

Two rules. First, create the chart once and then update it in place: in updateView, create the Highcharts instance only if it does not exist yet, otherwise call chart.update. Second, dispose it in the framework destroy method by calling chart.destroy and clearing the reference. Highcharts attaches SVG DOM and window resize listeners, so without an explicit destroy each mount of the control leaves a live chart behind.

Does Highcharts require a license?

Highcharts is free for personal and non-commercial use, but commercial projects require a paid Highcharts license. That licensing decision is separate from the technical integration described here. The same integration pattern also works with fully open-source libraries such as Chart.js or D3 if licensing is a constraint for your project.

Can PCF controls use other third-party libraries the same way?

Yes. The pattern in this guide is general: install the library from npm (or externalize it and load it from a CDN), initialize it against the container the framework hands you, update it in updateView instead of rebuilding it, and dispose it in destroy. It applies to D3, Chart.js, mapping SDKs, rich text editors, and most other JavaScript libraries you would embed in a model-driven app.

Can Solzet build a custom PCF control for our Dynamics 365 environment?

Yes. Custom PowerApps Component Framework development is a core Solzet service. We build tailored, high-performance controls, including data visualizations that wrap libraries like Highcharts, for Dynamics 365 Customer Engagement and the Power Platform, either directly for organizations or on a white-label basis for other Microsoft partners. Our team is based in Yerevan, Armenia, and delivers to clients across Europe and the US.

Need a custom PCF control built properly?

Solzet builds custom PowerApps Component Framework controls for Dynamics 365 Customer Engagement and the Power Platform, including data visualizations that wrap libraries like Highcharts. We deliver from Yerevan, Armenia, directly for organizations or on a white-label basis for other Microsoft partners. Tell us what you need to build.