Skip to content
PCF

PCF Code Components: When to Build One and How to Get It Right

The Power Apps component framework is the supported way to put your own rendering and behaviour inside a first-party app. It is also a maintenance commitment.

Rajiv KumarJune 3, 202614 min read
PCF Code Components: When to Build One and How to Get It Right

A PCF component is real code inside a low-code app, with a defined lifecycle and a real build pipeline. This covers the manifest, the lifecycle methods that trip people up, and the honest question of whether you should write one at all.

Every Power Platform team eventually hits a screen that standard controls can't quite deliver — a Gantt view the business insists on, a rating widget with a specific visual language, a grid that needs inline editing instead of the usual open-form-to-edit flow. The Power Apps Component Framework (PCF) exists for exactly this gap: it lets you drop real TypeScript-driven controls into canvas apps, model-driven forms, views, dashboards, and Power Pages, rendered in the same context as everything else on the page rather than bolted on as an iframe or a workaround.

The framework is also easy to reach for too early. A PCF component is a small piece of software with its own build pipeline, its own versioning rules, and its own solution-lifecycle quirks — and once it's in production, it behaves like any other dependency: someone has to keep it compiling against the next platform update. Knowing when that cost is worth paying, and knowing the handful of lifecycle details that separate a component that behaves from one that quietly degrades a form's performance, is most of what separates a good PCF decision from a regretted one.

What a Code Component Actually Gives You

A code component is not a canvas app "component" (the reusable groups of controls you build inside Power Apps Studio) — it's compiled code, built with the Power Platform CLI and TypeScript, packaged into a solution, and imported into an environment like any other customization. Once imported, it appears in the component picker for a column, a subgrid, a view, or a canvas app control, and it runs inside the host app's page — not in a separate frame — so it gets the same theming context, the same lifecycle events, and (with the right permissions) the same access to the platform's Web API.

Two things about licensing catch teams off guard late in a project, so it's worth settling them early:

Standard component

Premium component

Calls external services directly from the browser

No

Yes

Declared in manifest via

(default)

<external-service-usage enabled="true"> with <domain> entries

License required for end users

Existing Microsoft 365 / Dataverse license

Power Apps license

Typical use

Custom visualization of Dataverse/canvas data already in the app

Calling a third-party API or SDK directly from the component

Note: Adding an <external-service-usage> node to the manifest — even for a single external call — changes the app's licensing tier for every user who touches it. If a component only needs to talk to Dataverse or the data already flowing through the app's own data sources, leave that node out.

When PCF Is the Right Tool

The cases where a code component earns its cost tend to share one property: the interaction or visualization genuinely doesn't exist as a configuration option anywhere else in the platform.

Custom visualizations with real interaction logic. A Gantt chart with drag-to-reschedule, a network/relationship graph, a color picker with a business-specific palette, a calendar that needs day/week/month views with drag-and-drop rescheduling — these aren't styling problems, they're rendering and interaction problems that no column format or view layout setting solves.

Interaction patterns the platform doesn't offer. Inline editing inside a grid instead of opening a form, drag-and-drop reordering of records, a map control with custom pin behavior, a signature pad — these need code because the interaction model itself is different from what a standard grid or form control provides.

Third-party SDK integrations that need custom rendering. If a vendor's SDK renders its own UI (a mapping library, a charting library, a specialized input widget), a code component is usually the only way to host that rendering inside the app rather than working around it with an embedded web resource.

Genuinely reusable components. A rating control, a status badge renderer, or a formatted currency input that's going to appear on a dozen forms across three apps justifies the build cost once, because it's paid back every time it's reused — and updated once, in one place, rather than fixed in a dozen places.

When It Looks Like a PCF Problem but Isn't

This is where most wasted PCF effort actually happens — not on components that fail, but on components that get built to solve a problem a simpler platform feature already solves.

Symptom

Looks like

Actually needs

"This field needs to look different"

A custom field component

Column formatting, conditional formatting, or app theming

"We need to validate this before save"

Custom logic inside a component

A business rule, a Power Fx formula, or a Dataverse plug-in

"Just this one screen needs this widget"

A reusable PCF component

A canvas app custom component (no code, no solution ALM)

"We need to call this API from the form"

A PCF component with fetch logic

A custom connector or a Power Automate flow triggered from the form

Styling changes belong in theming because a component's visual language should track the app's theme automatically — a hard-coded color scheme inside a PCF bundle is exactly the kind of thing that goes stale the next time the brand guidelines change and nobody remembers which component owns the old palette. Validation belongs in business rules or Power Fx because that logic needs to run consistently across every surface that touches the record, not just the one form that happens to have the custom control. And a one-off need is almost always cheaper as a canvas app's own custom component — built and versioned inside the app itself, with no separate build toolchain, no solution import, and no CLI to keep updated.

Field vs. Dataset Components: Two Different Builds

PCF supports two component types, and the gap between them in implementation effort is bigger than the naming suggests.

Field components bind to a single column. They receive one value in context.parameters, render it, and raise notifyOutputChanged() when the user changes it. A star-rating control, a signature pad, or a custom slider are typical field components — the surface area is small, and pac pcf init --template field gets you a working scaffold in minutes.

Dataset components bind to a collection — a subgrid, a view, or a dashboard grid — and take on everything a grid normally handles: paging, sorting, filtering, and column configuration. This is a materially larger build. You're not just rendering a value; you're managing records, sortedRecordIds, paging, sorting, and filtering objects and keeping them in sync with what the user does on screen.

typescript

// Loading a specific page of a dataset
loadPage = (page: number): void => {
  this.currentPage = page;
  this.context.parameters.records.paging.loadExactPage(page);
};

// Building the ordered list of records to render
const sortedRecords = sortedRecordIds.map((id) => records[id]);

That gap in effort is the practical reason dataset components deserve more scrutiny before you commit to one — a Gantt view or a card-based board genuinely needs dataset-level control, but a lot of "we need a custom grid" requests are really "we need three columns formatted differently," which a view and conditional formatting handle without touching the framework at all.

The Lifecycle You're Actually Coding Against

Every code component implements the same four methods, and three of them have a sharp edge worth knowing before you ship.

Method

Called when

What to do

init(context, notifyOutputChanged, state, container)

Once, when the component first loads

Set up the component, read state if you persisted anything via setControlState(), append your UI to container

updateView(context)

Whenever the platform pushes new data or the container resizes

Update only what changed — see below

getOutputs()

Automatically, right after you call notifyOutputChanged()

Return the current output value(s)

destroy()

When the page unloads or the component is removed

Remove event listeners, clear timers, release references

Note: updateView fires far more often than a component actually needs to fully re-render — on every keystroke in some bound scenarios, on container resize, on any property change elsewhere on the form that the platform decides to propagate. Re-rendering your entire component from scratch on every call is the single most common cause of a PCF-heavy form feeling sluggish. Check context.updatedProperties to see what actually changed, and update only that.

The second sharp edge is the interaction between notifyOutputChanged() and updateView(). Calling notifyOutputChanged() tells the platform "my output changed, please call getOutputs()" — and the platform's response to a changed output is often to push a new context back into updateView(). If your updateView() logic calls notifyOutputChanged() unconditionally, you've built a loop: output changes trigger a view update, the view update triggers another output change, and so on. The fix is straightforward but easy to skip under deadline pressure: only call notifyOutputChanged() in direct response to genuine user interaction (a click, a keypress, a drag-drop completion), never as a side effect of processing an incoming updateView() call.

destroy() gets skipped more often than it should, and the failure mode is subtle: in a model-driven app, forms and their components are torn down and recreated as a user navigates between records, and any event listener you attached in init() without removing it in destroy() accumulates. One leaked listener is invisible. A few hundred, after a day of a user moving between records, shows up as a form that gets progressively slower with no obvious cause.

Building a Dataset Component: Sorting, Filtering, Paging

For a dataset component, the sorting and filtering APIs have one non-obvious rule: the sorting array is mutated in place, not replaced, so clearing it means popping every element rather than reassigning the property.

typescript

onSort = (name: string, descending: boolean): void => {
  const sorting = this.context.parameters.records.sorting;
  while (sorting.length > 0) {
    sorting.pop();
  }
  sorting.push({ name, sortDirection: descending ? 1 : 0 });
  this.context.parameters.records.refresh();
};

Filtering works similarly through filtering.setFilter() and filtering.clearFilter(), and both sorting and filtering changes should be batched before a single refresh() call rather than triggering a refresh per change — each refresh() is a round trip to reload the dataset, and firing several in quick succession (say, from a multi-column sort UI) causes visibly redundant network activity.

Tip: When rendering with React, wrap the record-to-row mapping in React.useMemo keyed on records, sortedRecordIds, and paging state. Dataset components tend to re-render on any dataset change, and recomputing a derived array on every render is wasted work the memoization avoids for free.

Resist the instinct to fetch "everything" into a local array and manage paging yourself — the dataset's own paging object (loadExactPage, hasNextPage, hasPreviousPage, totalResultCount) exists precisely so the platform continues to control how much data actually crosses the wire, which matters as much for a PCF dataset component as delegation does for a canvas app formula.

Should You Build with React and Platform Libraries?

pac pcf init offers --framework none (plain HTML/DOM, or any library you bundle yourself) or --framework react. For a small field component, plain HTML is often genuinely simpler — there's no benefit to pulling in a UI framework to render a slider or a badge. For anything with meaningful interactive state — a dataset grid, a Gantt view, a multi-step editor — React earns its place.

The detail worth knowing here is platform libraries. Historically, every PCF component that used React or Fluent UI bundled its own copy of those libraries, so a form with five React-based components loaded five separate React runtimes. Platform libraries let a component declare a dependency on a Microsoft-hosted version instead of bundling one:

xml

<resources>
  <code path="index.ts" order="1" />
  <platform-library name="React" version="16.14.0" />
  <platform-library name="Fluent" version="9.46.2" />
</resources>

This is what Microsoft calls a virtual component, and the payoff is real: a shared runtime instead of duplicated bundles means a dramatically smaller download per component and consistent Fluent styling across every control on the form, instead of each component's bundled Fluent version drifting slightly from the others. Microsoft's own figures put the bundle-size reduction from virtualization in the range of a 90%+ cut compared to a fully self-bundled component, though the exact number depends on what you'd otherwise be shipping. The specific React and Fluent version numbers the platform exposes do shift over time as Microsoft updates the shared libraries, so treat the versions above as illustrative and check the current platform-library reference before pinning a manifest.

Note: Not every component benefits from going virtual. A component that needs a very specific third-party charting library has no platform-library equivalent to lean on and will keep bundling its own dependencies regardless — platform libraries help specifically with the React/Fluent UI portion of the bundle.

Deployment Is an ALM Concern, Not a File Copy

The Power Platform CLI's pac pcf command group covers the full loop:

Command

Purpose

pac pcf init --namespace <ns> --name <name> --template field|dataset [--framework react]

Scaffold a new component project

pac pcf push --publisher-prefix <prefix> [--solution-unique-name <name>]

Build and import the component directly into a target Dataverse environment

pac pcf version --strategy gittags|filetracking|manifest [--allmanifests]

Bump the manifest version automatically as part of a build

pac pcf push is genuinely useful for iteration — it imports the component straight into a development or test environment without a manual solution export/import cycle, which is what makes the inner dev loop tolerable. But it's a development-time shortcut, not the production deployment path: a component destined for a managed production environment still needs to travel inside an exported solution through whatever ALM pipeline the rest of your customizations use, not pushed directly by a developer against production.

Version increments matter more than they look like they should, because of how Dataverse decides whether to serve a cached bundle or a new one. A patch version bump (the third number in the manifest version) is always enough on its own — the platform picks up the change. A major or minor bump, on the other hand, generally needs the solution's own version incremented alongside it, or Dataverse keeps serving the previously cached component bundle even though the solution import reported success. This is the exact failure mode behind "I imported the updated solution and nothing changed" — the import succeeded, the version just didn't move far enough (or the right version) to invalidate the cache. pac pcf version --strategy gittags (or filetracking, for teams that don't want to depend on git state at build time) exists to make this automatic in a CI pipeline, which is worth setting up the first time a version-bump miss costs someone an afternoon of confused debugging.

Common Mistakes Worth Checking For

A short list of the patterns that show up repeatedly once a PCF component reaches production, roughly in order of how often they're the actual root cause of a support ticket:

  • Unconditional full re-renders in updateView() — check context.updatedProperties before rebuilding the DOM or the React tree.

  • notifyOutputChanged() called from inside updateView() without a guard, creating an update loop that shows up as a form that never quite finishes loading.

  • Forgetting the version bump, or bumping only the manifest's patch number when a major/minor change actually needs the solution version moved too.

  • Fetching an entire dataset into memory in a dataset component instead of respecting paging, which quietly reintroduces the same row-limit problems that show up in non-delegable canvas app formulas.

  • Skipping destroy() cleanup, leaving event listeners attached across model-driven form navigation until the accumulation becomes visible as slowdown.

  • Declaring external-service-usage without realizing it moves every user of the app onto a premium license — a licensing decision that should be made deliberately, not discovered in a compliance review.

The Question to Ask Before You Start

The technical parts of PCF are well documented and, once you've built two or three components, genuinely predictable. The part that isn't documented anywhere is organizational: a code component is a small standing dependency — TypeScript, npm packages, a manifest schema, a CLI version — that someone has to keep compiling as the platform moves forward. A component nobody remembers how to rebuild is a liability the day a platform update breaks it, not before. Before committing to a build, it's worth being explicit about who maintains this in eighteen months, whether that person still has the toolchain installed, and whether the interaction the component provides is different enough from a standard control to be worth that ongoing cost. When the honest answer is "probably nobody," a slightly less elegant standard control that any maker can adjust from the UI is usually the better long-term decision — even against a PCF component that's genuinely well built today.

Frequently Asked Questions

Do I need React to build a PCF component? No. pac pcf init --framework none gives you a plain HTML/DOM scaffold, which is often the simpler and smaller choice for a field component with limited interactive state. React earns its cost on components with real UI complexity, like dataset grids or multi-step editors.

Can a code component call an external API directly? Yes, from the browser — but declaring that in the manifest via external-service-usage makes the component (and therefore the app) premium, which changes the licensing requirement for every end user. For most integration needs, a custom connector or a Power Automate flow keeps the app on standard licensing and centralizes the integration logic outside the component.

Does the same component work in both canvas apps and model-driven apps? A component can target both, and PCF's context and lifecycle APIs are largely consistent across hosts, but always verify behavior for your specific component in both surfaces — canvas apps and model-driven apps don't expose every context capability identically, and testing in only one host is a common source of "it worked in my dev environment" surprises.

How is a PCF component different from a canvas app's own custom component? A canvas app custom component is built inside Power Apps Studio with no code and no separate build tooling — it's the right choice for a one-off reusable group of controls inside a single app. A PCF code component is compiled TypeScript packaged into a solution, reusable across canvas apps, model-driven apps, and Power Pages, and it requires the Power Platform CLI, a publisher prefix, and solution-based ALM to deploy and version.

Is PCF supported on Power Pages? Yes — code components can run in Power Pages alongside canvas apps and model-driven apps, subject to the same manifest and licensing rules described above.

Closing Thought

PCF earns its place when a form genuinely needs an interaction or a visualization the platform doesn't offer out of the box — and it becomes a liability the moment it's used to work around styling, validation, or a one-off screen that a configuration option would have solved just as well. The lifecycle details that trip teams up — updateView firing far more than expected, notifyOutputChanged loops, version bumps that don't invalidate the platform's cache, dataset paging quietly getting bypassed — are all well understood and avoidable once you know to look for them. The harder discipline is upstream of any of that: deciding honestly whether the interaction is different enough to justify a standing piece of code, and naming who keeps it compiling next year, before the first line of TypeScript gets written.


Sources consulted: Code components overview – Microsoft Learn, Power Apps component framework overview – Microsoft Learn, Create a canvas app dataset component – Microsoft Learn, platform-library element – Microsoft Learn, React (virtual) and Fluent UI code components using platform libraries – Microsoft Power Platform Blog, pac pcf command group – Microsoft Learn, All about PCF versioning – Diana Birkelbach.

Share