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. The short answer: install Highcharts from npm and let the PCF build bundle it, create the chart once and update it in place in updateView, and call chart.destroy in destroy. Externalize it to a CDN only when bundle size is a real concern.

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.

Should you bundle Highcharts or externalize it and load it from a CDN?

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.

How do you integrate Highcharts into a PCF control 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

What does the complete Highcharts PCF control look like?

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;
  }
}

What are the common pitfalls with third-party libraries in PCF?

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.

What do developers ask about Highcharts in PCF?

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.

Should a React PCF control bundle its own React or use the platform-provided React?

Where you can, use the platform. A virtual control declares React and Fluent UI as platform libraries in its manifest, so the host supplies them and your bundle carries only your own code, which keeps it small and keeps styling consistent with the app. The trade-off is that your code and every React library you add must run on the React version the platform provides, which may be older than the one you would install from npm. If a library you need cannot run on that version, a standard control that bundles its own React is still valid. Check current Microsoft documentation for the supported versions.

Why is my PCF control bundle so large and how do I make it smaller?

Measure before changing anything. Build in production mode, check the size of bundle.js, and inspect its contents with a bundle analyzer or a source map explorer. The usual causes are a bundled copy of React or Fluent in a control that could borrow them from the platform, a whole utility, icon or charting library imported for one feature, unused locales or themes, and duplicate versions of the same dependency. Fix them with virtual controls, named ES module imports that tree-shake, modular packages, and moving rarely used heavy features to a separate control.

Can I lazy load parts of a PCF control with dynamic import?

Be careful. pcf-scripts packages a control as a single bundle declared in the manifest, so a dynamic import that makes webpack emit a separate chunk file can build without errors and then fail to load in Dynamics 365, because that file is not a declared resource. Check what your pcf-scripts version produces before relying on it. The dependable approach is to defer execution inside the one bundle, creating heavy features on first use instead of in init, or to split a rarely used capability into its own control.

Where should cleanup code go in a React PCF control?

In destroy, for everything that init or later calls created. For a standard control that renders React itself, that means root.unmount. For any control it also means removing document and window listeners with the same function reference, disconnecting ResizeObserver instances, destroying chart or editor instances, clearing timers and aborting in-flight requests. In a virtual control the host unmounts the React tree, but anything attached outside React still needs releasing, either in destroy or in the cleanup function of the effect that created it.

Why does my PCF control work in the test harness but fail after I import the solution?

Because the harness does not reproduce the target environment. The common causes for React and library heavy controls are an unchanged manifest version so the old bundle is still served, managed solution layering that keeps a different control on the form, a content security policy that blocks an externalised library, dataset paging that exposes only the first page of records, missing uses-feature declarations for WebAPI or Utility, a host without the expected platform library, and a development build that behaves differently from production. Test the exact production package in the target environment.

Should we migrate our existing standard PCF controls to virtual controls?

Only where it pays. Migrate standard controls that already bundle React and Fluent, particularly ones used several times on busy forms or whose styling clashes with the app. Leave controls that do not use React, such as a Highcharts control, stable controls due for retirement, and controls whose libraries need a newer React than the platform supplies. Measure every bundle first, pilot one migration through the target environment, then move the heaviest used controls with a version bump and a regression test each.

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.

We recommend the right solution: Dynamics 365 (Customer Engagement, Business Central or Finance and Operations), Power Platform, or a custom-built CRM. One team for CRM, ERP and the integration between them.

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.