Custom PCF Controls Development
Purpose-built PowerApps Component Framework controls in TypeScript and React - for model-driven apps, canvas apps, and Power Pages.
Solzet is a specialized PCF (PowerApps Component Framework) controls development team. We build custom UI components in TypeScript and React that extend Dynamics 365 and Power Apps beyond what out-of-the-box controls offer: interactive data grids, map visualizations, signature capture, rich text editors, drag-and-drop interfaces, and domain-specific widgets. Our developers work across the PCF lifecycle (init, updateView, getOutputs, destroy), Dataverse Web API integration, and responsive design for model-driven and canvas app hosting. Based in Yerevan, Armenia, we deliver PCF controls remotely to Microsoft partners and end clients across Europe and the US on B2B contracts. The guide below covers when PCF beats JavaScript, the build or buy decision, and planning, coding, testing, and deployment.
What is a PCF control and when do you need one?
A PCF (PowerApps Component Framework) control is a custom UI component written in TypeScript (optionally with React or other frameworks) that replaces or enhances a standard field, dataset, or section in a Dynamics 365 or Power Apps form. You need a PCF control when the out-of-the-box controls cannot deliver the user experience your business requires - for example, an interactive map showing customer locations pulled from a Dataverse table, a Gantt chart for project scheduling, a barcode scanner for warehouse operations, a rich signature capture pad for field service, or a custom data grid with inline editing, conditional formatting, and export capabilities. PCF controls are the Microsoft-recommended successor to legacy HTML web resources, offering better performance, lifecycle management, and cross-device compatibility.
PCF or traditional JavaScript: which does your requirement need?
Traditional JavaScript on Dynamics 365 means a script web resource registered against form and field events through the client API, sometimes wrapped in an HTML web resource when the requirement needs markup of its own. It is not deprecated, it is not going away, and for a meaningful share of the requests we are asked to quote it is still the cheaper correct answer. So the useful question is not which of the two is more modern. It is which of the two you want to own in three years, on a form maintained by somebody who did not write it, after six release waves have landed on it.
Five criteria decide it in practice, and they are the first five rows of the matrix below: future proofing, reusability, solution packaging, complexity of the interface, and maintenance overhead. Two further rows are there because they settle real cases more often than any of the five: where the code is allowed to run at all, and what each route costs across three years rather than in the sprint it was built in.
That last row is where a feature comparison stops being useful. A form script is cheap to write and cheap to deploy, and its real cost is not in the build, it is in the copies. The same fifty lines end up on three forms, in two solutions, and in a form somebody cloned for a new business unit, and the day the rule changes the work is finding them rather than changing them. A code component costs more before anyone sees it, because a manifest, a build, a version number and a managed solution all exist first, and after that it stays one artifact with one version everywhere it is used. The crossover point is not a line count. It is the second place the behaviour has to appear, or the first time somebody outside the original team has to change it.
What sits either side of this decision is written up in full elsewhere on the site rather than repeated here. If the matrix says component, our PCF controls library is the first stop rather than the last, because a maintained component that already does the job removes the build entirely, and PCF controls: buy, build, or customize costs the three routes side by side including the number most teams work out only in year two. The decision tree further down this page then settles who writes it. Where the verdict is a component because the interface needs a real rendering engine behind it, integrating third party libraries such as Highcharts into a PCF control is the pattern that keeps that component cheap to own rather than expensive to keep.
- Future proofing. Both routes are supported, so this is a surface area question rather than a deprecation question. A script reaches into the form through the client API and depends on the shape of what it manipulates, so anything that touches the document object model directly breaks the first release wave that changes the rendering. A code component is handed a container and talks to the platform through a versioned framework contract, which is the narrower and more stable surface. Where a requirement has to still be working in five years, that difference is most of the argument.
- Reusability. A script is reused by being copied, or by being referenced from a second form and hoping nobody edits it for one caller. A component is reused by being added to a form and configured, with its behaviour parameterised in the manifest rather than hard coded, so the second and third use cost minutes instead of another round of testing. One behaviour in one place is a script question. The same behaviour in several places, or across several apps, is a component question.
- Solution packaging. Both ship inside a solution and neither behaves the same way once it is there. A script web resource is a file with no version of its own, so two solutions carrying the same file overwrite each other silently and telling the copies apart afterwards means opening them. A code component carries a version in its manifest that the platform uses for caching and upgrade, ships through the managed layer, and leaves a previous artifact you can import to roll back to. If application lifecycle management matters to whoever signs off your releases, this row is often settled before the interface is even discussed.
- Complexity of the interface. This is the criterion people lead with and the easiest to judge. Changing values, visibility, requirement level and enabled state on controls the platform already renders is client API work, and doing it with a script is both supported and quick. Rendering something the platform has no control for, meaning a board, a map, a timeline, a chart, or a grid with its own editing behaviour, is component work, because the only way to draw a new interface from a form script is to inject markup into a form you do not own. There is no supported version of that, and the unsupported versions are exactly what fails on upgrade.
- Maintenance overhead. A script has a low floor and no ceiling: nothing to maintain until something changes, then an unbounded search for every copy and every form that loads it. A component has a real floor, meaning a build, dependencies, and somebody named for the next release wave, and a predictable ceiling after that, because there is one artifact to test and one version to bump. Budget the floor honestly. A control with no owner after go live is a liability whatever the rest of the matrix said.
CRITERION JAVASCRIPT WEB RESOURCE PCF CODE COMPONENT
----------------------------------------------------------------------------
Future proofing Supported, and tied to A versioned contract with the
the form surface it platform and a container it
touches. Unsupported DOM hands you. Far less surface
work breaks at a release to break.
wave.
Reusability Reused by copying the Added to a form and
file, or by one form configured. Behaviour
quietly editing it for parameterised in the
every other caller. manifest, not in the code.
Solution packaging A file carrying no Versioned in the manifest,
version of its own. The upgraded through the managed
last import wins and layer, with a rollback
nothing tells you it did. artifact.
Interface Changes what the platform Renders what the platform
complexity already renders: values, does not ship: boards, maps,
visibility, requirement timelines, charts, custom
level, enabled state. grids.
Maintenance No floor and no ceiling. A real floor: build,
Nothing to own until a dependencies, a release wave
change, then find every owner. A predictable ceiling
copy first. after that.
Where it runs Model-driven forms only. Model-driven forms and grids,
Not canvas apps, not canvas apps, custom pages,
custom pages, not Power Power Pages.
Pages.
Cost over 3 years Hours to build. The cost Days to build and package.
arrives later, as copies, The cost is known up front
drift and an upgrade and stays one artifact.
nobody owns.
----------------------------------------------------------------------------
Read the rows together. One row rarely settles a case on its own.
Three or more pointing the same way always does."Format the account number as it is typed, on one form"
-> SCRIPT. One field, one form, one behaviour on a control the
platform already renders. A manifest, a build and a version
number bought for nothing.
"Warn the seller and block the save when the discount passes a limit"
-> SCRIPT, and only a script. A code component owns its own
container and has no supported way to register on the form save
event, so it cannot do this at all.
"Colour code the status choice wherever it appears"
-> COMPONENT, and probably one that already exists. Generic
behaviour, wanted on more than one form, and exactly what a
controls library is for. Adopt before you build.
"Show the bookings for this resource as a drag and drop board"
-> COMPONENT, mandatory. The platform does not ship this surface,
and drawing it from a form script means injecting markup into a
form you do not own, which breaks on a release wave.
"Plot the records of this view on a map, with the pins clickable"
-> COMPONENT, mandatory, for the same reason plus a second one. A
dataset binding hands the control the records of the view, which
a form script has no route to.
"The same margin calculation, on four forms and in a canvas app"
-> COMPONENT, decided by reach rather than by complexity. Form
scripts do not run in a canvas app, so the script route is four
copies plus a rewrite in Power Fx, maintained in two languages.The honest answer is often both
A code component owns the container the platform gives it and nothing outside it. Form load and save orchestration, tab and section visibility, command bar rules, and navigation to another record all sit outside that container, so they stay client API work however modern the rest of the form is. A well built form frequently carries a component for the surface people work in and a small script for the orchestration around it, and that is a correct design rather than a compromise. A supplier who tells you every line of JavaScript on your forms can become a control is quoting for a rewrite you do not need.
The same logic runs in reverse on the code you already have. Do not migrate a working script because a component is newer. Migrate when a row in the matrix has actually changed: the behaviour now has to appear where a script cannot run, the copies have drifted until nobody can say which is authoritative, the script manipulates the document object model in a way the next release wave will break, or the release process needs one versioned artifact instead of a file. Where none of those is true, the rewrite is cost with no return and we will say so before quoting it. Where several are true at once, the migration becomes the same three route decision as anything else, which is the buy, build, or customize guide.
How does Solzet build PCF controls?
Our PCF development process starts with a detailed specification: what data the control reads and writes, which Dynamics 365 form or canvas app it lives in, and the exact UX requirements including responsive behavior. We then build the control using TypeScript and React (our standard choice for complex UIs), leveraging the Fluent UI component library for visual consistency with the Dynamics 365 interface. Development follows Microsoft best practices: proper use of the ComponentFramework.Context API, efficient rendering with updateView, cleanup in destroy, and manifest configuration for bound/input/output properties. We test across browsers (Edge, Chrome, Firefox) and devices (desktop, tablet, mobile). Delivery includes the managed solution (.zip) ready for import into your Dataverse environment, plus source code and documentation so your team can maintain it.
How do you develop PCF controls for Dynamics 365?
This is the guide the rest of the page runs on, written for whoever has to deliver a code component rather than evaluate one. It answers three questions in order. Should this control exist at all. What actually happens in each of the four phases of PCF controls development. And what the whole thing looks like end to end, from the first scoping call to the production import, on a real Dynamics 365 project rather than on a clean tutorial environment.
Start with the decision, because a meaningful share of the PCF requests we are asked to quote should never become a PCF project. The tree below is the version we run in a scoping call and it takes about twenty minutes. Five questions, five verdicts, and only one of those verdicts is a new control. Reaching the wrong verdict is expensive in a specific way: a component nobody needed still has to be versioned, tested against two release waves a year, and owned by somebody after go live.
Two of the verdicts have a full guide of their own on this site, because they deserve more than a branch on a tree. PCF controls: buy, build, or customize costs the three routes side by side, including the year two number most teams work out too late, and gives worked verdicts for schedulers, map pickers, and large dataset viewers. Our own PCF controls library is where the adopt branch starts, since a maintained component that already does the job is the cheapest correct answer on this page.
- Configure. A supported first party feature covers most of it: the modern grid, the editable grid, a first party map control, a business rule, a better view, or an embedded canvas app. Take the eighty percent and stop. No manifest, no version, no owner.
- Adopt. The behaviour is generic and a maintained component already does it. Import it, configure it on the form, and name somebody to own it at the next release wave. Judge the component by when it was last updated rather than by how popular it looks.
- Customize. A component is close but not exact. Worth it only when the licence allows a fork and the delta is genuinely small, because from the moment you edit it you own the merge for every future update. If the delta is not small, price it as a build.
- Build in house. The behaviour is yours, the form is not business critical, and you can spare a developer for the learning curve as well as for the code. The four phases below are written so that you can run them without us.
- Commission. The behaviour is yours and the blast radius is large: every seller or every service agent touches it, or it writes to data that invoicing depends on. A written specification and a fixed price move the risk of a misunderstanding onto the supplier, which is the actual thing being bought.
A business request for something the form cannot do today
|
1. Does a supported first party feature already cover most of it?
|-- yes -> CONFIGURE. Modern or editable grid, first party map,
| business rule, view, embedded canvas app. Stop here.
'-- no -> question 2
|
2. Is the behaviour generic, or does it encode your own process?
|-- generic -> question 3
'-- yours -> question 5
|
3. Does a maintained component already do it?
(our library and the community galleries; read the last commit
date, not the star count)
|-- yes -> question 4
'-- no -> question 5
|
4. Does it fit with configuration alone, no behaviour changes?
|-- yes -> ADOPT. Import, configure, name a release wave owner.
'-- no -> CUSTOMIZE when the licence allows a fork and the
delta is small. Otherwise price the fork as a build
and go to question 5.
|
5. How many people touch it, and what does it write to?
|-- few users, low blast radius -> BUILD IN HOUSE, if you can
| spare the learning curve.
'-- many users, invoiced data,
business critical form -> COMMISSION against a signed
specification at a fixed
price. Phase 1 starts here.What does the PCF control development lifecycle look like, from planning to deployment?
The official framework documentation shows you how to make a control run on your own machine. It stops there. It does not tell you how to specify a control so the estimate survives contact with the business, how to keep updateView cheap when the form it sits on already carries a dozen other customisations, how to test something the local harness cannot reproduce, or how to get the result through a change board and into production with a version number you can roll back to. That gap is where PCF projects actually fail, so this is our full lifecycle, written out so you can run it in house or hold our delivery to it.
Four phases, each with a deliverable rather than a feeling of progress. Planning produces a signed specification and a manifest. Coding produces a control that respects the component lifecycle instead of fighting it. Testing produces evidence from a real environment, not only from the harness. Deployment produces a versioned managed solution and a repeatable path from development to production. The most expensive mistake we are called in to fix is a project that started at phase two, because a control written before anyone agreed what it reads, what it writes, and where it lives becomes a rewrite rather than a change request.
The decision tree in the guide above settles whether the control should exist, so everything from here assumes the verdict was build or commission. The one question left before phase one is who owns each phase, and the team against freelancer comparison works that through: what a PCF engagement contains beyond TypeScript and React, and the screening questions that tell a developer who has shipped controls into production from one who has read about them.
- Phase 1, planning. Deliverable: a written specification, an agreed ControlManifest.Input.xml, and a decision on host, control type, and data access. Typically days, not weeks, and it is the phase that fixes the price.
- Phase 2, coding. Deliverable: a control that does expensive work once in init, cheap work in updateView, hands values back through getOutputs, and releases everything in destroy.
- Phase 3, testing. Deliverable: harness runs for the fast loop, plus evidence from a real environment under a real security role, with a real dataset, on the devices your users actually hold.
- Phase 4, deployment. Deliverable: a versioned managed solution, an import that is repeatable in test and production, a rollback position, and a named owner for the next two release waves.
Phase 1: how should a PCF control be planned before any code is written?
Planning a code component is not a design exercise, it is a contract exercise. Four decisions carry almost all of the cost, and all four are cheap to change now and expensive to change once there is a codebase. First, the host: model-driven app form, editable grid, custom page, canvas app, or Power Pages, because that decides which platform APIs exist for you. Second, the control type: a field control bound to one column, or a dataset control bound to a view, which are different components with different manifests rather than two settings of one thing. Third, standard or virtual: a virtual control borrows React and Fluent from the platform, so the bundle is smaller and the theming matches the host, while a standard control carries its own rendering stack and is the safer choice when you need a library the platform does not provide. Fourth, data access: everything through bound properties and the dataset, or a declared Web API feature for reads the binding cannot reach.
Then write down the behaviour in the language of the people who asked for it, not in React terms. What happens on an empty record, on a read only form, for a user whose security role hides one of the columns, at ten rows and at ten thousand, and offline on a phone. Every answer you do not write down becomes a change request later, and change requests are where fixed price projects turn into time and materials.
The manifest is where those decisions become binding, so we agree it before development starts. Anything you might want to configure per environment or per form belongs there as an input property, because a value in the manifest is changed by an administrator in seconds while a value in the code is a rebuild, a version bump, and a solution import. Declare feature usage explicitly as well: context.webAPI, device access, and the utility APIs simply do not exist at runtime unless the manifest asks for them, and the failure mode is an undefined object in production rather than a build error in development.
- Data in and data out. Every bound, input, and output property with its exact type, and what the control does when a value is null or the user has no read access to it.
- Host and form context. Which app, which form or view, whether the same control must also run in a canvas app, and what it does when the form is read only or the record is inactive.
- Volume and performance budget. Expected and worst case row counts, page size, and the form load time the control must not exceed. Write the number down, then test against it in phase three.
- Security expectations. Which roles see what, whether field level security applies to any bound column, and whether the control must degrade gracefully rather than error when a read is denied.
- Configurability. Anything you would otherwise hard code: colours, thresholds, labels, target tables, page size. Each one becomes an input property in the manifest.
- Accessibility and localisation. Keyboard operation, screen reader labels, high contrast, right to left, and which languages need resx string files.
- The deliverable itself. Source repository, managed and unmanaged solution artifacts, the publisher prefix, documentation, and who owns the code and the upgrade after go live.
- Definition of done. The specific scenarios that will be demonstrated at sign off, agreed now rather than negotiated at the end.
<?xml version="1.0" encoding="utf-8" ?>
<manifest>
<control namespace="Solzet" constructor="OrderGrid" version="1.0.0"
display-name-key="OrderGrid" description-key="Editable order grid"
control-type="virtual">
<!-- Dataset control: bound to a view, not to a single column. -->
<data-set name="records" display-name-key="Orders"
cds-data-set-options="displayCommandBar:true;displayQuickFind:true">
<property-set name="amount" display-name-key="Amount"
of-type="Currency" usage="bound" required="true" />
</data-set>
<!-- Configurable by an administrator per form. No rebuild required. -->
<property name="pageSize" display-name-key="Page size"
of-type="Whole.None" usage="input" required="false" />
<property name="highlightThreshold" display-name-key="Highlight above"
of-type="Decimal" usage="input" required="false" />
<!-- What the control hands back to the platform. -->
<property name="selectedIds" display-name-key="Selected ids"
of-type="SingleLine.Text" usage="output" required="false" />
<!-- Without this, context.webAPI is undefined at runtime. -->
<feature-usage>
<uses-feature name="WebAPI" required="true" />
</feature-usage>
<resources>
<code path="index.ts" order="1" />
<platform-library name="React" version="16.8.6" />
<platform-library name="Fluent" version="9.46.2" />
<resx path="strings/OrderGrid.1033.resx" version="1.0.0" />
</resources>
</control>
</manifest>Phase 2: how is the control coded against the component lifecycle?
The framework gives you four methods and almost every performance problem we are asked to fix comes from putting work in the wrong one. init runs once, so it owns the expensive things: metadata reads, option set label lookups, security role checks, third party library initialisation, event subscriptions. updateView runs constantly, on every dependent property change, every dataset page, every container resize, and sometimes for reasons that have nothing to do with your control, so it owns cheap, idempotent rendering and nothing else. getOutputs runs when the platform asks for values after you called notifyOutputChanged, and it must be a pure read of state rather than a place where work happens. destroy runs when the control leaves the DOM, and skipping it is how a user who scrolls a subgrid for ten minutes ends up with a leaking browser tab.
Two habits separate a control that survives its second year from one that gets rewritten. Use context.updatedProperties to find out what actually changed instead of rebuilding everything, and create your rendering surface once and update it in place rather than tearing it down. Where a third party library is involved, both habits become mandatory, because charting and mapping libraries attach their own listeners and SVG nodes. Our walkthrough of integrating third party libraries such as Highcharts into a PCF control covers that pattern in full, including webpack externals against bundling and the disposal code that prevents the leak.
Dataset controls need one more discipline. The platform hands you a page, never the whole result set, so read paging.totalResultCount before you write any logic that assumes the grid holds everything, and call loadNextPage deliberately in response to a user action rather than in a loop that walks the entire table on form load. On the write side, prefer bound and output properties over direct Web API writes, because bound values go through the platform save pipeline where business rules, plugins, and audit already work.
- Expensive once, cheap often. Metadata, labels, role checks, and library setup in init. Rendering in updateView. Nothing surprising in getOutputs.
- Render once, update in place. Keep the instance, apply new data to it, and never rebuild the DOM tree on every updateView. This is the single largest cause of flicker and of leaked nodes.
- Release everything in destroy. Event listeners, timers, observers, subscriptions, and any third party instance you created. Assume the control will be mounted and unmounted hundreds of times per session.
- Guard the optional platform. context.webAPI exists only when the manifest declared it and the host provides it, so a control that must also run in a canvas app has to fail soft rather than throw.
- Respect the container. Call context.mode.trackContainerResize(true) and lay out against allocatedWidth and allocatedHeight rather than against window dimensions, or the control will be wrong inside a subgrid and wrong again on a phone.
- Respect read only and disabled states. Check context.mode.isControlDisabled and the field level security flags on bound properties before rendering anything editable.
- Type against the generated types. Run refreshTypes after every manifest change so ManifestTypes.d.ts matches the contract, and the compiler catches a renamed property instead of production catching it.
- Keep the bundle honest. A virtual control using platform React and Fluent, plus tree shaking and an externalised heavy library, is the difference between a form that loads and a form that people complain about.
export class OrderGrid
implements ComponentFramework.ReactControl<IInputs, IOutputs> {
private notifyOutputChanged: () => void;
private selectedIds: string[] = [];
private currencyFormat: string;
public init(
context: ComponentFramework.Context<IInputs>,
notifyOutputChanged: () => void,
): void {
this.notifyOutputChanged = notifyOutputChanged;
// Once only. Metadata reads, label lookups and library setup belong here,
// never in updateView, which the platform may call hundreds of times.
this.currencyFormat = context.userSettings.numberFormattingInfo.currencySymbol;
context.mode.trackContainerResize(true);
}
public updateView(
context: ComponentFramework.Context<IInputs>,
): React.ReactElement {
const records = context.parameters.records;
// Do not render a half loaded dataset, and do not rebuild the tree when
// only the container size moved.
if (records.loading) {
return React.createElement(GridSkeleton);
}
return React.createElement(Grid, {
records,
readOnly: context.mode.isControlDisabled,
width: context.mode.allocatedWidth,
threshold: context.parameters.highlightThreshold.raw ?? 0,
currencySymbol: this.currencyFormat,
onSelect: (ids: string[]) => {
this.selectedIds = ids;
this.notifyOutputChanged();
},
});
}
public getOutputs(): IOutputs {
// A pure read of state. No work, no side effects, no async.
return { selectedIds: this.selectedIds.join(',') };
}
public destroy(): void {
// Every listener, timer and third party instance created above is released
// here. Skipping this is how a long session leaks a tab.
}
}// The platform hands you one page, never the whole table. Read the total
// before writing anything that assumes the grid holds every row.
const ds = context.parameters.records;
const pageSize = context.parameters.pageSize.raw ?? 100;
if (ds.paging.pageSize !== pageSize) {
ds.paging.setPageSize(pageSize);
}
// Load on a user action. Never loop loadNextPage on form load: a 40,000 row
// table will do exactly what you told it to.
if (ds.paging.hasNextPage && this.userRequestedMore) {
this.userRequestedMore = false;
ds.paging.loadNextPage();
}
// context.webAPI is undefined unless the manifest declared the feature and the
// host supports it. Canvas hosting does not, so fail soft rather than throw.
const canQuery = typeof context.webAPI?.retrieveMultipleRecords === 'function';
const related = canQuery
? await context.webAPI.retrieveMultipleRecords(
'account',
'?$select=name,revenue&$filter=statecode eq 0&$top=50',
)
: { entities: [] };Phase 3: how do you test a PCF control before it goes anywhere near production?
Testing happens in two places and teams that only use the first one ship the bugs we get paid to find. The local harness, started with npm start watch, is the fast loop: sample data, a resize slider, a container size control, and a reload on every save. It is excellent for layout, formatting, and interaction work. What it cannot do is the part that breaks in production. There is no real Dataverse behind it, so context.webAPI is not available, security roles do not apply, field level security does not apply, paging behaves nothing like a real view, and the form context around your control does not exist.
So the second place is a real development environment, with the control on a real form, holding real volumes, opened by a user who is not a system administrator. That last point catches more defects than any other single test. A control built and demonstrated by an administrator will happily hide a query the business user is not allowed to run, and it will fail on the first day of user acceptance testing rather than in your sprint. Create a test user with the actual production role, remove your own privileges for an hour, and open the form.
Alongside that, keep pure logic out of the framework so it can be unit tested normally. Sorting, grouping, threshold evaluation, currency and date formatting, and validation rules should live in plain functions that a test runner can call without a host, which is also what makes them cheap to reverify after each release wave. Two release waves a year plus your own React and component library upgrades mean a control is never finished being tested, only tested most recently.
- Harness first, environment second. Layout, resize, and interaction in npm start watch. Everything involving data, permissions, or volume in a real environment.
- Test as a non-administrator. A test user with the production security role, and where relevant a role with field level security restrictions on a bound column.
- Test at real volume. The worst case row count from the specification, not twenty sample rows, with paging exercised and the form load budget measured in browser developer tools.
- Test the empty and broken states. New record with no data, deleted related record, null in a required bound column, and a denied read. None of these should produce a blank control or a stack trace.
- Test the hosts you promised. Unified interface on desktop, the mobile app on a real phone, and if the specification said so, a canvas app, where the Web API is unavailable and data arrives through Power Fx.
- Test read only and disabled forms. Business process locked records, inactive records, and forms where the control is disabled by a business rule.
- Test accessibility. Keyboard only operation from tab in to tab out, screen reader labels on interactive elements, and high contrast mode.
- Unit test the logic, not the framework. Plain functions for calculation and formatting, with a test runner, so the release wave regression check takes minutes instead of a day.
npm run refreshTypes # regenerate ManifestTypes.d.ts after any manifest change
npm run build # type check and bundle, treat warnings as failures
npm start watch # local harness: layout, resize and interaction only
npx jest --coverage # the logic that does not need a host to be correct
# Reality check the bundle before anyone sees the form.
# A heavy control on a busy form is a support ticket waiting to happen.
ls -lh out/controls/OrderGrid/bundle.jsPhase 4: how are deployment, versioning and the release checklist handled?
A control is deployed as part of a solution, and the rules that govern solutions are what most tutorials skip. Development environments get unmanaged, everything downstream gets managed, and the artifacts for both come from the same build so what you tested is what you import. pac pcf push is a development convenience that puts an unmanaged control straight into your development environment: it is genuinely useful in the inner loop and it should never be how anything reaches test or production, because it leaves you with no versioned artifact to roll back to.
Version discipline is the other half. Increment the version in ControlManifest.Input.xml on every release and increment the solution version with it. If you do not, the platform can serve a cached copy of the previous bundle and you will spend an afternoon debugging code that is not running. After any import, publish all customizations, then hard refresh, then verify against the actual form rather than against the solution import log. Where two solutions both contain the control, remember that the last managed import wins in the layer, which is why a shared publisher prefix and a single owning solution matter more than they sound.
Finally, decide who owns the control after go live, and write it into the statement of work rather than leaving it implied. Two release waves a year, plus dependency updates in React and the component library you chose, mean a control drifts quietly until a change request or a browser update exposes it. That ownership question is exactly the one covered in our comparison of hiring a PCF team against a freelancer, and it is the difference between a component you maintain and a component you eventually replace.
When an import into a managed or production environment fails outright, work through the causes in a fixed order rather than retrying. First, the publisher prefix. A code component is identified by the publisher prefix, the namespace and the constructor name together, so the same control packaged under a different prefix is a different component to the platform, not a new version of the old one. The classic route into that mess is pac pcf push during development, which creates its own temporary solution under whatever prefix was passed, followed by a release solution under the real publisher: the target now holds two controls with almost the same name, and forms stay bound to the one you did not update. Fix it by settling one publisher per product, removing the stray development component from development, and rebinding forms to the control from the owning solution.
Second, the version numbers, which follow two separate rules. The version attribute in ControlManifest.Input.xml must increase for an updated bundle to be picked up; an import with the same control version can succeed and still serve the old code. The solution version must also be higher than the version already installed for a managed upgrade, and importing a lower version over a higher one is refused rather than treated as a rollback. Rolling back therefore means a new, higher version built from the previous source, which is why the pipeline stamps both numbers instead of leaving them to memory.
Third, read the import log for the real error. The message on the screen is usually generic. Download the log from the solution import history in the maker portal, or read the output of pac solution import, and look for the component rows marked as failed: the error text and code on that row name the missing dependency, the conflicting component or the owning solution that blocks the change. Missing dependencies are listed by component, so the fix is either to add them to the solution or to import the solution that owns them first.
Fourth, environment policy. A Managed Environment can enforce solution checker results so that a solution with critical findings is blocked or warned at import, and administrators can restrict unmanaged customizations or who may import at all. Data loss prevention policies govern connectors rather than code components, so they rarely block the control itself, but they can leave flows in the same solution switched off after import, which looks like a broken release. Code components in canvas apps also need the environment setting that allows them. These settings and their names change, so check the current Power Platform admin documentation and ask the tenant administrator what is enforced before a two day deadline turns into a policy conversation.
At scale, the same discipline becomes a pipeline. For a partner or ISV maintaining several controls across many client tenants, each control lives in its own repository and builds through one shared pipeline template: install, production build, version stamping, pac solution pack or a solution project build that produces the managed artifact, and the artifact stored with its version. Each client receives a managed solution pinned to the control versions they are on, deployed with a service principal registered as an application user in each environment rather than a named account, and every import is followed by verification: the installed version read back with pac solution list and a smoke test on the real form. An inventory of which control version runs in which tenant is what stops drift. The wider move from manual ZIP files to that kind of release, including deployment settings per environment and a shared core for many clients, is set out in our guide to Power Platform ALM with PAC CLI instead of manual export and import.
Import failures and how to clear them, in the order to check them.
| Symptom | Usual cause | How to clear it |
|---|---|---|
| Two controls with nearly the same name in the target, forms still show the old one | Publisher prefix mismatch, often a development component created by pac pcf push under a different prefix | One publisher per product, remove the stray component from development, rebind forms to the control in the owning solution. |
| Import succeeds, form runs the old bundle | Control version in ControlManifest.Input.xml not increased | Increase the manifest version and the solution version, publish, hard refresh, verify on the form. |
| Import refused as an older version | Solution version lower than the managed version already installed | Build a new, higher version, including for a rollback built from previous source. |
| Import fails with missing dependencies | A table, column, choice or other component the solution references is not in the target | Read the failed component rows in the import log, then add the dependency to the solution or import its owning solution first. |
| Import blocked or warned before it starts | Managed Environment solution checker enforcement or another environment policy | Run the solution checker in the pipeline, fix critical findings, and confirm enforced settings with the tenant administrator. |
| Import completes, related flows are off | Data loss prevention policy or connection references not set for that environment | Supply connection references through a deployment settings file and review the policy with the administrator. |
- Version bumped. ControlManifest.Input.xml and the solution version both incremented, and the change recorded against a work item.
- Built clean. A fresh build from the committed source with no local uncommitted changes, producing managed and unmanaged artifacts from the same run.
- Publisher and prefix correct. The same publisher across environments, because changing it later means a new component rather than an update.
- Dependencies present. Every table, column, view, choice, and security role the control assumes exists in the target environment, or is in the same solution.
- Import path proven. The managed solution imported into a test environment that resembles production, not only into the environment it was built in.
- Customizations published and verified. Publish all customizations, hard refresh, then open the real form as a real user and confirm the new behaviour is actually running.
- Rollback position known. The previous managed solution artifact kept and importable, and the team agreed in advance on what triggers a rollback.
- Performance re-measured in production. Form load timed on the production form, not on the developer machine, because production has more customisations, more data, and slower networks.
- Handover complete. Source in your repository, manifest decisions documented, configuration properties explained to whoever administers the app, and IP ownership stated in writing.
- Owner named for the next release wave. A person or a supplier with a support arrangement, agreed before go live rather than looked for afterwards.
- Target environment policy checked. Solution checker enforcement, restrictions on unmanaged customizations and import permissions confirmed with the tenant administrator before release day.
- Import verified, not assumed. The import log read for failed component rows, the installed solution version read back, and the version recorded in the inventory of which control runs in which environment.
# Once, in a folder alongside the control project.
pac solution init --publisher-name solzet --publisher-prefix slz
pac solution add-reference --path ../OrderGrid
# In the .cdsproj, so one build produces both artifacts:
# <SolutionPackageType>Both</SolutionPackageType>
# Every release. Bump the manifest version first, then build.
dotnet build --configuration Release
# Development only. Fast inner loop, unmanaged, never a promotion path.
pac pcf push --publisher-prefix slz
# Promote the managed artifact through test, then production.
pac auth create --environment https://contoso-test.crm4.dynamics.com
pac solution import --path bin/Release/Solution.zip --publish-changes --async
# Then verify on the form as a non-administrator, not in the import log.# Build stage, identical in every control repository.
npm ci
npm run build -- --buildMode production
# Stamp versions from the pipeline, never by hand.
# Flag names move between CLI releases: check pac pcf version --help
# and pac solution version --help for the release you run.
pac pcf version --patchversion $(Build.BuildId)
pac solution version --buildversion $(Build.BuildId)
# Produce the managed artifact and keep it with its version.
dotnet build ./Solution --configuration Release
# Release stage, once per client environment, as a service principal
# registered as an application user in that environment.
pac auth create --applicationId $(ClientAppId) --clientSecret $(ClientSecret) \
--tenant $(ClientTenantId) --environment $(ClientEnvironmentUrl)
pac solution import --path ./Solution/bin/Release/Solution_managed.zip --async
# Verify: read the installed version back and record it in the inventory,
# then run the smoke test on the real form.
pac solution listThe deployment failure we are called about most
The control works in development, the managed solution imports into production without an error, and the form shows the old behaviour. Nine times out of ten the manifest version was not incremented, so the platform kept serving the cached bundle. The remaining cases are usually a missing dependency the development environment happened to have, a feature declared in one solution but imported from another, or customizations that were never published. All four are checklist failures rather than code failures, which is why the checklist above exists and why we run it on every release rather than on the ones that feel risky.
The urgent version of the call is different: the import into a customer managed environment fails, and the release is due in two days. The order we work in is the one in the table above. Download the import log and find the failed component row before touching anything, because the generic message on screen almost never names the cause. Check whether the control in the target was created under a different publisher prefix, often by an earlier pac pcf push or a previous supplier, and whether the solution version is higher than the one installed. Then check dependencies the development environment had and the target does not, and finally ask the tenant administrator whether solution checker enforcement or another Managed Environment setting is blocking the import. Many of these clear quickly once the real error is known. For partners and ISVs who hit this across many client tenants, the lasting fix is the shared pipeline and version inventory described above, which we build and run directly or white-label.
How should third party libraries be handled, and what does a control look like from scoping to production?
Most controls worth commissioning need something the platform does not ship: a charting engine, a mapping SDK, a timeline or scheduler renderer, a rich text editor, a document viewer. How that library reaches the browser is decided in phase one and paid for in phase four, so it belongs in the specification next to the manifest rather than in a pull request three weeks later. There are four patterns, and the choice between them is usually made by the environment rather than by taste.
Whichever pattern you pick, the integration rules are the ones the component lifecycle already implies: create the instance once, update it in place, and release it in destroy. A charting or mapping library attaches listeners and nodes of its own, so a control that rebuilds the instance on every updateView will flicker on a busy form and leak a tab over a long session. The full code for that pattern, with Highcharts as the worked example, is in our guide to integrating third party libraries into a PCF control, including the webpack externals configuration and the disposal code, so it is not repeated here.
The log below puts the four phases and the library decision together on a single control. It is a composite of engagements rather than one named client, with the days rounded, and it is the same shape as the eight controls two of our developers built white label for a UK Microsoft Solutions Partner in the PCF controls development case study. Read it as the answer to how long a Dynamics 365 control takes and where the time actually goes, which is rarely where people expect.
- Platform library. React and Fluent declared in the manifest and borrowed from the host. Smallest bundle, theming that follows the environment automatically, and the default for a virtual control on a model-driven form.
- Bundle it with npm. Install, import, and let the build pipeline place the library inside bundle.js. Self contained, works where outbound script loads are blocked, and the right default for a small library or a locked down tenant. The cost is bundle size on every form load.
- Externalise it and load from a content delivery network. The import resolves to a global at runtime and several controls share one cached copy. Cheaper on a form carrying multiple controls, and it adds two failure modes: the network has to be reachable from managed devices, and the environment content security policy has to allow it.
- Keep it off the form entirely. Some libraries do not belong in a code component: heavy rendering, anything needing a server side licence key, or a document engine that is happier as a service. Do the work in an Azure function and let the control display the result.
- Settle licensing before any code. Commercial charting, scheduling, and grid libraries are licensed per developer, per application, or per domain. That question is cheap in the specification and expensive once a control has been written against the library.
Day 1 Scoping call. The ask: a drag and drop scheduling grid
on the work order form. Run the decision tree first. The
first party schedule board is ruled out because bookings
live in a custom table the client already reports on.
Verdict: commission.
Day 2-3 Specification. Host: model-driven form. Type: dataset
control, virtual, platform React and Fluent. In: six
bound columns. Out: selected ids plus a moved booking
payload the form saves. Volume: 400 bookings a week,
worst case 2,000 rows in view. Budget: form load under
two seconds with the control on it. Roles: dispatchers
move bookings, technicians are read only.
Day 3 ControlManifest.Input.xml agreed and signed with the
specification. pageSize, slotMinutes and the colour
thresholds become input properties, because those are
the three things clients change twice during user
testing. Fixed price set. Everything after this is a
change request rather than an argument.
Day 4 Library decision. The timeline renderer is not something
the platform ships. Bundled with npm rather than loaded
from a network, because the client blocks outbound
script loads on managed devices. Licence purchased
before the first commit.
Day 5-12 Build. init reads user settings, time zone and choice
labels once. updateView renders, then updates the same
instance in place. getOutputs returns the moved booking.
destroy disposes the renderer and removes the resize and
drag listeners.
Day 13 Harness pass. Layout, resize, three container widths,
empty state, single row, and a null in a bound column.
Day 14 Real environment. 2,000 rows imported, paging exercised
deliberately, form load measured at 1.4 seconds in
browser developer tools against the two second budget.
Day 15 Opened as a technician test user rather than as an
administrator. Two defects the harness cannot produce: a
denied read on a cost column threw, and a read only form
still allowed a drag. Both fixed the same day.
Day 16 Accessibility pass, keyboard only from tab in to tab
out, then the mobile app on a real phone.
Day 17 Version incremented in the manifest and in the solution.
Managed and unmanaged artifacts built from committed
source in one run.
Day 18 Import into a test environment, publish all
customizations, hard refresh, verify on the real form as
a real user. Previous managed artifact kept for
rollback.
Day 19 Production import in the next release window. Form load
re-measured in production. Handover pack delivered and
an owner named for the next release wave.Where the time actually goes
Nineteen working days in that log, and eight of them are the build. The rest is the specification that fixes the price, the testing a local harness cannot do, and the deployment discipline that keeps a rollback available. Teams that estimate a control by its build alone are quoting less than half of the work, which is the usual reason a fixed price on PCF development stops being fixed.
What lands at the end is the same on every engagement: managed and unmanaged solutions built from committed source, the full repository, the manifest decisions documented so an administrator knows which behaviour is configurable without a developer, and IP ownership stated in the statement of work. If you already have a specification, send it and we will mark up the manifest and the phases that carry the risk before quoting. If you do not have one, the decision tree above is the first conversation.
Can a custom PCF control run entirely inside your tenant with no outbound calls?
Yes, and for a buyer with a strict data boundary that should be a written design constraint rather than an assumption. A code component runs in the user's browser inside the Dynamics 365 or Power Apps page. If every value it shows arrives through its bound properties, its dataset, or context.webAPI, then every read and write goes to your own Dataverse environment under the signed-in user's security roles and column security, exactly as a form would. Nothing in the framework requires a control to call anything else. Outbound traffic only appears when a developer adds it: a library loaded from a content delivery network, a web font from a third party host, an error tracking or analytics SDK, a map or geocoding service, or a fetch to an API somebody found convenient.
So a tenant-only control is mostly a list of things it does not do, enforced at build time and proven in the browser. Libraries are bundled from vetted npm packages rather than externalised to a network location, which is the right default for a locked down tenant even though it costs bundle size, as the third party library patterns above explain. Fonts and icons come from the platform and Fluent UI. Errors are logged to the browser console, or written to a Dataverse table under the user's own privileges if support needs them, never to an outside telemetry service. The manifest declares no external service usage. And the evidence is a browser network capture of the control in use, showing requests to your environment and nowhere else, kept with the release.
The rest of what makes a control acceptable to a compliance reviewer is ordinary engineering done deliberately: dependencies vetted, large datasets handled without dragging whole tables into the browser, accessibility checked, and nothing that a release wave can break. The eight controls in our PCF controls development case study for a UK Microsoft partner, delivered white label for a healthcare organisation, were built in TypeScript, React and Fluent UI, committed to the partner's Azure DevOps repository through full pull request review, and packaged as managed solutions. When a control that passed testing then fails on a real form, our reference on PCF controls in production works through the causes.
- No outbound calls. All data through bound properties, the dataset and context.webAPI, under the user's Dataverse privileges. No fetch or XMLHttpRequest to other hosts, no external service usage declared in the manifest, and a network capture of the control in use kept as release evidence.
- Dependency vetting. Every package justified in the specification, versions pinned by a committed lockfile and installed with a clean install in the pipeline, licences reviewed for commercial and copyleft terms before any code is written, known vulnerability checks run on each build, and packages that run install scripts or fetch resources at runtime rejected.
- No third-party CDN, fonts or telemetry. Libraries bundled rather than loaded from a network, fonts and icons from the platform and Fluent UI, and no analytics, session replay or error tracking SDKs in the bundle.
- Performance on large datasets. The platform hands a dataset control one page at a time, so the control pages deliberately, filters and sorts through the dataset rather than in the browser, and virtualises long lists so only the visible rows are rendered. The worst case row count goes in the specification and is tested at that volume.
- Accessibility checks. Keyboard operation from tab in to tab out, screen reader names and roles on interactive elements, visible focus, and high contrast, checked with automated tooling during development and by hand before sign off.
- Update resilience. Supported framework APIs only, no reaching into the host form markup outside the control's container, no undocumented context members, and a test pass in a sandbox with early access enabled before each release wave.
- Build, packaging and ALM. TypeScript and React built in production mode from source in your repository, managed solutions produced by a pipeline rather than on a developer machine, versions incremented in the manifest and the solution on every release, and promotion through test into production as described in our Power Platform ALM guide.
When is a PCF control the wrong answer in a compliance-constrained tenant?
More often than a supplier who sells controls tends to admit. Every component added to a regulated environment is code somebody has to vet, own and retest at each release wave, so the cheapest compliant control is frequently the one you do not build. The table compares the four options we actually weigh for a requirement that a standard form cannot meet. It is a compliance view of the decision; the full cost and ownership comparison of buying, building and customizing a control is in PCF controls: buy, build, or customize.
The deadline matters as much as the table. A control that has to pass a security review of its dependencies, an accessibility check and a release through test before production is not a two week job, and a specification still being argued about means the estimate is not real yet. If the go live date is fixed and close, ship the native view or modern grid now, document what users cannot yet do, and commission the control for the following release with a signed specification. A control rushed to meet a date is the one that reaches for a CDN library, skips the non-administrator test and becomes the audit finding.
Four ways to meet a requirement a standard form cannot, compared for a tenant with a strict data boundary. Microsoft features and licensing change, so confirm current behaviour for your environment before deciding.
| Option | Right answer when | Compliance considerations | Wrong bet when |
|---|---|---|---|
| Native view or modern grid | The need is filtering, sorting, inline editing, conditional display or grouping of records the platform already shows. | No custom code to vet. Security, auditing and accessibility are the platform's own, and release waves are Microsoft's responsibility. | The interface genuinely does not exist in the platform, such as a board, timeline, map or domain-specific editor. |
| Embedded canvas app | A makers team can build the screen, the logic is data entry or lookup across related records, and Power Fx is enough. | Configuration rather than bundled code, but connectors must stay within your data policies, and every connector added is a potential outbound path to review. | It needs pixel level interaction, very large lists, or has to feel like part of the form rather than a frame inside it. |
| Power BI report or visual | The need is analysis and visual summary rather than editing, and the organisation already governs Power BI. | Data moves into the Power BI service, so check where that capacity is hosted, who can export, and whether any custom visual is approved by your administrators. | Users must act on records in the flow of work, or the data cannot leave Dataverse at all. |
| PCF control | The interaction must live on the form or grid, the platform has nothing equivalent, and someone will own it after go live. | Custom code in the page: tenant-only data access, vetted bundled dependencies, no CDN or telemetry, accessibility and release wave testing, as set out in the section above. | The deadline does not leave room for specification, dependency review and testing, or nobody will own the control at the next release wave. |
Why do custom PCF controls break on every platform update, and how do you take them over?
A PCF control breaks on update when it depends on something the framework never promised: unsupported DOM access outside its container, APIs it never declared in the manifest, and library versions pinned years ago that drift from what the host now provides. Taking such controls over starts with triage, not code. Inventory the manifest, dependencies and API usage of each control, run Solution Checker, and mark which controls use unsupported patterns. Then decide per control whether to harden it, replace it with an out-of-the-box control, or rebuild it. Finally, put in the handover standard that stops it recurring: source control, a build pipeline, and a documented upgrade test before each release wave.
This section is about code components specifically. If the breakage is wider than your controls, with form scripts, plug-ins and unmanaged layers failing on the same update, the general diagnosis, the disable-not-delete routine and the pre-release gate are on common Dynamics 365 customization mistakes and how to fix them, and we do not repeat them here. What follows is the part that page does not cover: why a component that is meant to sit behind a stable framework contract still breaks, and what a takeover of a set of controls looks like when the developer who wrote them has left.
The framework contract is narrow on purpose. The platform hands a control a container, a context object and the properties declared in its manifest, and it keeps that surface stable across release waves. Everything outside it is the host implementation, which Microsoft changes whenever it needs to. A control that stays inside the contract rarely notices an update. A control that reaches outside it notices every one, and the symptoms are always the same: a blank field, a grid that renders twice, a control that works for administrators and fails for everybody else, or styling that collapses after the interface refresh.
After the inventory, every control gets one of three verdicts. The table is how we decide, and the decision is recorded per control rather than applied to the whole set, because a takeover of eight controls rarely ends with eight identical answers.
| Verdict | When it is the right call | What the work actually is |
|---|---|---|
| Harden | The source exists and builds, the behaviour is still wanted, and the unsupported patterns are few and local: one DOM query, one undeclared feature, an outdated library that can move forward. | Declare the features it uses, replace DOM access with context and manifest properties, move libraries to supported versions or platform libraries, add the destroy cleanup, increment the version and release through a managed solution. |
| Replace with out-of-the-box | The platform has caught up since the control was written. The modern grid, the editable grid, a first party control or a small configuration change now covers most of what the control does. | Swap the control on the form back to a first party control in a solution change, check what users lose, agree it with the process owner, then retire the component. The cheapest verdict, and the one a previous developer has least incentive to suggest. |
| Rebuild | No usable source, the design itself depends on the host markup, or hardening would touch most of the code anyway. Also right when the control now has to run somewhere new, such as a canvas app or a custom page. | Treat it as a new control with the existing one as the specification: write down what it really does in production, agree the manifest, and build through the four phases above so the new version is maintainable from day one. |
- Unsupported DOM access. Code that queries the form outside its own container, walks up to parent or window.top, reads host CSS class names to find a tab or a field, or injects global styles that override the host. The host markup is not part of the contract and changes without notice, so these controls break on the day the rendering changes rather than on a date anyone announced.
- Undeclared or unsupported APIs. context.webAPI, device and utility calls used without a matching feature-usage entry, undocumented members of the context object found in a debugger, or form logic driven through the Xrm global from inside a control. Some of these work for years by accident, then return undefined after a service update, and nothing fails at build time to warn you.
- Pinned library versions. A bundled copy of React, Fluent UI or a charting library frozen at the version the original developer installed, a platform-library declaration that assumed the host would stay on the same version, or build tooling so old that the project no longer installs on a current Node.js. The control keeps running until the host and the bundled copy disagree, and then nobody can rebuild it quickly because the build itself is broken.
- No reproducible source. The only artifact is the managed solution in production. The bundle inside it can be read, but a minified bundle is evidence rather than something you can maintain, so a missing repository changes the likely verdict on its own.
The triage we run on each control before any verdict
Manifest first: control type, standard or virtual, every bound, input and output property, the feature-usage declarations, the platform-library declarations and the version number, which tells you whether releases were ever versioned properly. Dependencies second: the package file, the lock file if there is one, the build tooling version, and every library bundled rather than borrowed from the host. API usage third: every call through context, every reference to window, document, parent or Xrm, and every global style. Then Solution Checker over the solutions that carry the controls, kept as evidence, and a read of the source itself where it exists, because a checker working through a minified bundle sees less than a person reading the TypeScript.
The result is a one-page register: the control, the form and process it sits on, who uses it, the unsupported patterns found, the verdict and an estimate. That register is what a service owner can budget against and what an admin-heavy team can keep current afterwards.
What handover standard stops taken-over PCF controls breaking again?
Controls rarely break again because the fix was wrong. They break again because the conditions that let them drift are still there: the source lives on one laptop, the build only works on one machine, and nobody tests the control until users report it after a release wave. The handover standard below is what we leave behind on every takeover, written so that a team made up mostly of administrators and makers, with little TypeScript in house, can own the controls without becoming developers.
The choice of who does the code work afterwards is a separate decision, and it is the one our comparison of hiring a PCF team against a freelancer works through, including what a deliverable must contain. Most of the controls we are asked to take over were left behind by exactly the arrangement that page warns about: one person, no repository and no named owner for the next release wave. Where the controls are one symptom of a wider stalled implementation, the assessment extends to the whole environment through our Dynamics 365 project rescue and takeover service.
- Source control you own. Every control in a repository in your GitHub or Azure DevOps organisation, with the manifest, the package and lock files, and a short readme covering what the control does, where it is used and how to build it.
- A build pipeline, not a build machine. A pipeline that installs, builds and packages the managed solution from committed source, so a fix can be released in hours by anyone with access rather than by the one person whose machine still works.
- A documented upgrade test before each release wave. Early access switched on in a sandbox copy of production, a short script per control covering the forms it sits on, a non-administrator role and a real dataset, and a named person who signs off before the wave reaches production.
- A register the admin team can maintain. The control, the forms it is on, its configurable input properties, its owner and its last verified release wave, kept next to the solution inventory rather than in a personal inbox.
- A switch-off routine that needs no developer. How to set a field or grid back to its default control on the form in a solution change, so a failing control can be taken out of the user path in minutes without deleting anything.
- Supported patterns enforced at build time. Lint rules and Solution Checker in the pipeline, so a change that adds DOM access or an undeclared API cannot reach test or production quietly.
How do you scope and price a custom PCF control?
PCF control effort varies significantly by complexity. A simple field-level control (formatted display, custom input mask) is a quick engagement. A medium-complexity control (interactive chart, filtered lookup with custom UI) takes more time. A complex dataset control (full custom grid, map visualization, drag-and-drop scheduler) is a larger project. All engagements include specification, development, testing, managed solution packaging, and documentation. Solzet offers volume pricing for Microsoft partners who need multiple controls built across client projects. Pricing is always transparent and fixed-price after specification approval - contact us for a detailed quote.
Why do companies choose Solzet for PCF development?
If you are working through a list of Power Apps PCF development companies, the question that decides the shortlist is not who can write TypeScript and React. It is who is still responsible for the control eighteen months from now. Most of what a search returns is one of two things: a GitHub repository of open source components published by a community author, or an individual freelance developer on a marketplace. Both are legitimate, and where the component you need already exists and is actively maintained, taking it is the cheapest correct answer. Solzet is the third option on that list, and a different kind of supplier: a specialist Dynamics 365 Customer Engagement and Power Platform consultancy where PCF development is a contracted service with a written specification, a fixed price after approval, a named team, and an owner after go live.
Four things separate us from the rest of the list. Senior only delivery: every control is built by a senior engineer who has shipped controls into production, not by a junior learning the component lifecycle on your form, and our team holds Microsoft certifications PL-200, PL-400, and PL-600 alongside the Dynamics 365 Customer Engagement certifications MB-210, MB-230, and MB-240. White label subcontracting: Microsoft partners and systems integrators hire us under their own brand, using their email, their Azure DevOps or GitHub, and their communication channels with the end client, which is how a partner adds TypeScript and React capacity for one programme without hiring for it permanently. Rescue and takeover: we routinely adopt controls somebody else started, abandoned, or left undocumented, beginning with an audit of the manifest, the lifecycle, and the solution packaging before any new code is written, the narrow form of our Dynamics 365 project rescue and takeover service. Cost base: we deliver from a single hub in Yerevan, Armenia, so certified senior engineers cost less than the Western European consultancy equivalent, and GMT+4 gives 4 to 6 hours of daily overlap with Western European hours and 6 to 8 with the UK.
We also publish our own components rather than only building them to order, and our PCF controls library is the fastest way to judge the work before you commission anything, since the free controls in it are built by the same engineers to the same standard as commissioned ones. The practical difference between the three routes on your list comes down to six factors, and it is worth being explicit about them because they are the ones a repository README and a marketplace profile do not answer.
- Support after go live. Specialist consultancy: support and enhancement are part of the engagement, done by the engineers who wrote the control, with a monthly allocation or ad hoc hours on a B2B contract. Open source repository: an issues tab, answered when the author has time, and no commitment that it will be answered at all. Marketplace freelancer: available while the contract runs, and a message into the void once it ends.
- IP ownership and what you actually receive. Specialist consultancy: a managed solution ready to import, the full source in your repository, the manifest choices explained, and IP ownership defined in the statement of work, so the control is yours. Open source repository: whatever the licence permits, which is usually generous but binds you to the author choices and gives you no warranty. Marketplace freelancer: negotiable, and frequently the gap in the contract, which is how buyers end up with a working control they cannot rebuild.
- Project reliability and continuity. Specialist consultancy: a team rather than a person, so illness, holiday, or somebody leaving is a scheduling problem instead of a stalled project. Open source repository: no delivery commitment exists, because nothing was promised to you. Marketplace freelancer: one person, one calendar, and one point of failure.
- Quality, and who does the screening. Specialist consultancy: the screening happened before the engineer joined us, and PCF is what the firm does every week rather than an occasional job. Open source repository: quality varies from excellent to abandoned, and reading the code is your job. Marketplace freelancer: a strong React profile does not prove anyone has packaged a managed solution or honoured dataset paging, so the screening is yours to run.
- Release waves and year two. Specialist consultancy: two Microsoft release waves a year are tested against your controls before they land, and somebody owns the upgrade. Open source repository: the control drifts on the author schedule, and a component untouched for two waves is the real warning sign rather than a low star count. Marketplace freelancer: nobody owns the upgrade unless you re-engage the same person, if they are still reachable.
- Cost and the commercial shape. Specialist consultancy: fixed price per control after the specification is approved, volume pricing for partners commissioning several across client projects, and no recruitment fee, payroll, or notice period. Open source repository: free to acquire, and priced instead in the engineering time it takes to review, adapt, and maintain it. Marketplace freelancer: the lowest headline rate of the three, with change requests and rework as the variable nobody quotes for.
What this looks like in practice
A UK Microsoft Solutions Partner won a Dynamics 365 programme that required eight custom PCF controls and had no in-house TypeScript and React capacity for them. Two of our senior developers joined their delivery team white label for three months, working in the partner Azure DevOps and appearing to the end client as the partner. The full delivery, including what was built and what was handed over, is written up in the PCF controls development for a UK Microsoft partner case study.
If you are still deciding between supplier types rather than between suppliers, our guide to hiring PCF developers as a team or as a freelancer works through the same decision in more depth, including where to find open source controls, when a free component or a freelancer is genuinely the right call, and the screening questions to ask whoever you hire.
Can Solzet build PCF controls for canvas apps?
Yes. PCF code components are supported in canvas apps as well as model-driven apps, and we build controls that run in both. The same TypeScript and React codebase serves both hosts. What changes is how data reaches the control: a canvas app has no Dataverse column to bind to, so the app supplies values through input properties set with Power Fx, or a dataset bound to the Items property, and the control hands results back through output properties that raise OnChange. Two things catch teams out, and neither means canvas is unsupported: the component framework has to be switched on per environment before code components appear in Power Apps Studio, and the Dataverse Web API is not available to a control running in a canvas app, so data access belongs on the app side. This is particularly powerful for organizations that use canvas apps for mobile scenarios but need richer UI than standard canvas controls provide - things like camera integration, offline-capable data entry, or custom calendar views.
Where can you go deeper?
PCF controls: buy, build, or customize?
Before commissioning anything, work out which of the three routes your control belongs in. The guide gives the five questions that decide it, the three routes costed side by side including what each one costs after year one, and worked verdicts for schedulers, map pickers, and large dataset viewers.
Integrating third party libraries into a PCF control
The coding phase in full detail, using Highcharts as the worked example. Bundling against webpack externals, loading a library from a CDN, creating the instance once and updating it in place, and the disposal code in destroy that stops a busy form leaking a chart on every record you open.
Hiring PCF developers: a team against a freelancer
Once building is ruled out, this decides supplier type. What a PCF engagement contains beyond React and TypeScript, where each buying route holds up or falls over, and the screening questions that separate a developer who has shipped controls into production from one who has read about them.
Common Dynamics 365 customization mistakes and how to fix them
When the breakage on update is wider than your PCF controls. Unsupported form scripts, plug-in design, solution layering, what to do on the day an update breaks something, and the pre-release gate that makes the next release wave a non-event.
Power Platform ALM: PAC CLI instead of manual export and import
The release discipline behind a multi-control, multi-client PCF pipeline. Solution source in a repository, version stamping, managed only downstream, deployment settings per environment, and a shared core across many client tenants.
What do buyers ask us?
What is the PowerApps Component Framework (PCF)?
The PowerApps Component Framework (PCF) is Microsoft's official framework for building custom code components that run inside Power Apps (both model-driven and canvas) and Dynamics 365 forms. PCF controls are written in TypeScript, can use React or other JavaScript frameworks, and follow a defined lifecycle (init → updateView → getOutputs → destroy). They replace the older approach of HTML web resources and offer better performance, mobile compatibility, and integration with the Power Platform ecosystem.
Should I use PCF controls or HTML web resources?
PCF controls are the Microsoft-recommended approach going forward. HTML web resources are legacy and come with limitations: they run in iframes (poor mobile experience, security restrictions), cannot access the ComponentFramework API, and do not work in canvas apps. PCF controls render natively in the form, have full API access, support responsive design, and are distributable as managed solutions. If you have existing web resources, Solzet can help migrate them to PCF controls.
When should I use a PCF control instead of traditional JavaScript?
Use a JavaScript web resource when the behaviour changes something the platform already renders, on a form where scripts run, in one place: setting values, visibility, requirement level and enabled state, validating on save, defaulting from a related record, or formatting a single field. Use a PCF control when the interface itself has to be something the platform does not ship, such as a board, a map, a timeline, a chart or a grid with its own editing behaviour; when the same behaviour has to appear on several forms, in a canvas app or on a custom page, where form scripts do not run at all; or when your release process needs one versioned artifact with a rollback position rather than a file that the last solution import silently overwrites. The decision matrix on this page compares the two routes on future proofing, reusability, solution packaging, interface complexity, maintenance overhead, where each can run, and what each costs across three years, with worked verdicts for six typical requests.
Should we rewrite our existing JavaScript web resources as PCF controls?
Not because a code component is newer. Rewrite when something has actually changed: the behaviour now has to appear somewhere a form script cannot run, such as a canvas app or a custom page; the copies have drifted across forms and solutions until nobody can say which one is authoritative; the script manipulates the form document object model in unsupported ways that a release wave will break; or your release process needs a versioned artifact with a rollback position instead of a file with no version of its own. Where none of those is true, a working script is cheaper to keep than to replace, and we say so before quoting the work. A code component also cannot replace everything: form load and save orchestration, tab and section visibility, command bar rules and navigation sit outside the container a control owns, so those stay client API work.
Can I hire Solzet to build PCF controls as a white-label subcontractor?
Yes. Many Microsoft partners hire Solzet to build PCF controls under their brand for their end clients. We work under NDA, use your project tooling and communication channels, and deliver controls that your team packages and deploys. This is ideal for partners who need TypeScript/React expertise for a specific project without hiring a permanent developer.
What technologies does Solzet use for PCF development?
Our standard PCF stack is TypeScript + React + Fluent UI, built with the PAC CLI (Power Platform CLI) and bundled with Webpack. For simpler controls we use vanilla TypeScript without React. We test with PCF Test Harness during development and browser DevTools for runtime debugging. Version control is Git (GitHub or Azure DevOps), and we deliver managed solutions (.zip) for import into your Dataverse environment.
How do I choose between Power Apps PCF development companies?
Compare on six factors rather than on rate. Who supports the control after go live and under what contract. Who owns the source, the managed solution, and the documentation when the engagement ends. Whether delivery depends on one person or on a team. Who screened the engineer for Dataverse and solution ALM as well as for TypeScript and React. Who owns the upgrade at each of the two Microsoft release waves a year. And what the commercial shape is: a fixed price after an approved specification is a very different purchase from an hourly rate against an unwritten scope. A repository of open source controls and an individual freelancer answer some of those and leave the rest to you. Solzet answers all six in the statement of work.
Why choose a specialist PCF consultancy over free components on GitHub?
Often you should not, and we say so on our own comparison guide. If a maintained open source control does what you need, take it, and check when it was last updated rather than how many stars it has. A consultancy earns its fee where that breaks down: when the control has to know your business rules, when the form it sits on is business critical, when nobody in house can adapt or repackage the component, or when something has to survive the next release wave with an owner attached. What you are buying then is not the code, which is the cheap part. It is accountability for the code: a support arrangement, defined IP ownership, and a team rather than an issues tab.
Can Solzet take over or fix a PCF control built by someone else?
Yes, and it is a regular part of our PCF work. We start with an audit rather than a rewrite: the manifest and property definitions, what runs in init against what is repeated on every updateView, whether state is released in destroy, how the control behaves against a real dataset with paging and a restricted user, and how the solution is versioned and packaged. That produces a written picture of what you have, what is actually wrong, and the smallest change that fixes it, so a rebuild is a decision you take with evidence rather than a default. Controls left behind by a freelancer who is no longer reachable, or by a partner who has stepped away, are the common cases. The section on this page about why custom PCF controls break on every platform update sets out the full takeover triage, the harden, replace or rebuild verdict per control, and the handover standard that stops the breakage recurring.
What are the steps in the PCF control development lifecycle?
Four phases. Planning: agree the host, the control type, standard against virtual, the data access route, and the ControlManifest.Input.xml, which is the contract the platform actually reads. Coding: expensive work once in init, cheap idempotent rendering in updateView, a pure read in getOutputs, and full cleanup in destroy. Testing: the local harness for layout and interaction, then a real environment at real volume opened by a non-administrator, because the harness has no Dataverse, no security roles, and no realistic paging. Deployment: a versioned managed solution built from committed source, imported into test before production, published, verified on the real form, with a rollback artifact kept and an owner named for the next release wave.
How do you test a PCF control before deploying it?
Start in the local test harness with npm start watch for layout, resize, and interaction, then move everything that involves data or permissions into a real development environment. The harness cannot reproduce the failures that matter: context.webAPI is unavailable, security roles and field level security do not apply, paging does not behave like a real view, and there is no form context. The highest value test is opening the form as a test user with the real production security role rather than as a system administrator, followed by testing at the worst case row count from the specification, on the mobile app, on read only and inactive records, and with keyboard only navigation.
What should be on a PCF deployment checklist?
Increment the version in ControlManifest.Input.xml and in the solution. Build managed and unmanaged artifacts from the same clean build of committed source. Confirm the publisher and prefix match the environments. Confirm every table, column, view, and choice the control assumes is present in the target or ships in the same solution. Import the managed solution into a test environment that resembles production first. Publish all customizations, hard refresh, then verify on the real form as a real user rather than trusting the import log. Keep the previous managed artifact so rollback is possible. Re-measure form load in production. Complete handover of source, manifest decisions, and configuration properties, and name the owner for the next release wave.
Why does my PCF control not update after importing the solution?
Almost always the version. If the version in ControlManifest.Input.xml was not incremented, the platform can keep serving the cached bundle, so the import succeeds and the form shows the previous behaviour. Increment the manifest version and the solution version together, publish all customizations after the import, then hard refresh the browser. The other common causes are a dependency the development environment happened to have and the target does not, the same control shipped in two solutions where the last managed import wins the layer, and a feature such as the Web API that was never declared in the manifest, which makes context.webAPI undefined at runtime rather than failing at build time.
Why does a PCF control solution import fail in a managed environment?
Check the causes in order. A publisher prefix mismatch makes the platform treat the control as a different component, often because an earlier pac pcf push created it under another prefix. A solution version lower than the managed version already installed is refused. A missing dependency, such as a table or column the target lacks, fails the import and is named on the failed component row of the import log. And a Managed Environment policy, such as solution checker enforcement, can block the import before it starts. Download the import log rather than relying on the generic message, and confirm current environment settings with the tenant administrator.
What are the version numbering rules for upgrading a deployed PCF control?
There are two numbers and both matter. The version attribute in ControlManifest.Input.xml must increase for the platform to pick up an updated bundle; if it does not change, the import can succeed and users keep running the old code. The solution version must be higher than the managed version already in the target for the upgrade to import. A rollback is therefore a new, higher version built from the previous source. Stamp both numbers in the build pipeline, for example with pac pcf version and pac solution version, rather than editing them by hand.
How do you build and deploy multiple PCF controls from separate repositories to many client environments?
Give every control repository the same shared pipeline template: install, production build, version stamping, packing the managed solution and storing the artifact with its version. Keep one publisher per product so prefixes never diverge. Release a managed solution to each client pinned to the control versions they are on, authenticate with a service principal registered as an application user in each environment, and verify every import by reading the installed version back and running a smoke test on the form. Keep an inventory of which version runs in which tenant. Our Power Platform ALM guide covers the wider move from manual export and import to this model.
Should a PCF control be a standard control or a virtual control?
A virtual control borrows React and Fluent from the platform, so the bundle is much smaller, form load is faster, and the styling follows the host theme without extra work. That is the default we reach for on model-driven forms. A standard control carries its own rendering stack, which is the right call when you need a library the platform does not provide, when you are wrapping a third party visualisation or mapping SDK, or when you need a React version the platform library does not offer. The decision belongs in planning, because switching later is a rewrite of the rendering layer rather than a configuration change.
How do you develop a PCF control for Dynamics 365, and how long does it take?
Five steps, in this order. Run the build against buy decision tree, because configuring a first party feature or adopting a maintained component is often the correct answer. Write the specification and agree ControlManifest.Input.xml, which fixes the host, the control type, standard against virtual, data access and every configurable property. Build against the component lifecycle: expensive work once in init, cheap idempotent rendering in updateView, a pure read in getOutputs, full cleanup in destroy. Test in the local harness for layout, then in a real environment at real volume as a non-administrator. Deploy a versioned managed solution built from committed source, through test into production, with a rollback artifact kept. On the worked example above, a medium complexity dataset control with a third party renderer took nineteen working days, of which only eight were the build itself. A simple field control is considerably faster, and a large scheduler or map control considerably slower.
When should I build a PCF control instead of buying one?
Build or commission when the behaviour encodes your own process rather than a generic pattern, and when no maintained component fits with configuration alone. Buy or adopt when the behaviour is generic, which is most tagging, formatting, colour coding and viewer requirements. Configure and build nothing when a supported first party feature such as the modern grid, the editable grid, a first party map control or an embedded canvas app covers most of it. Customizing an existing component is the narrow middle case: worth it only when the licence permits a fork and the delta is small, because from that point you own the merge for every future update. Two further factors decide the route when the tree is close: how long the control has to live, and how many people it touches, since a control on a business critical form used by everybody justifies a written specification and a supplier who carries the risk.
How do I use a third party JavaScript library such as a charting engine in a PCF control?
There are four patterns. Borrow React and Fluent from the platform by declaring them as platform libraries in the manifest, which gives the smallest bundle and host theming. Bundle the library with npm and let the build pipeline put it inside bundle.js, which is self contained and works where outbound script loads are blocked. Externalise it with webpack and load it from a content delivery network, which keeps the bundle small and lets several controls share one cached copy, at the cost of network reachability and a content security policy allowance. Or keep it off the form entirely and do the work in a service when the library is heavy or needs a server side licence key. Whichever you choose, create the library instance once, update it in place on updateView, and dispose of it in destroy, or a busy form will flicker and leak. Settle commercial licensing before any code is written.
Can a custom PCF control run in Dynamics 365 without sending any data outside our tenant?
Yes. A control that takes all of its data from bound properties, its dataset and context.webAPI only talks to your own Dataverse environment, under the signed-in user's security roles. Outbound traffic only appears if a developer adds it, through a library loaded from a CDN, third party web fonts, a telemetry or error tracking SDK, or a call to an outside API. Make its absence a written requirement, bundle vetted libraries instead, and keep a browser network capture of the control in use as evidence with each release.
How should third party dependencies in a PCF control be vetted for a regulated environment?
Justify every package in the specification, pin versions with a committed lockfile and install them with a clean install in the build pipeline, review each licence before code is written, run known vulnerability checks on every build, and reject packages that run install scripts or fetch resources at runtime. Bundle what remains rather than loading it from a content delivery network, and keep the dependency list with the release so a reviewer can see exactly what shipped.
When is a native view, an embedded canvas app or a Power BI report better than a PCF control?
A native view or the modern grid is better when the need is filtering, sorting, inline editing or conditional display, because there is no custom code to vet or retest. An embedded canvas app suits data entry a makers team can build with Power Fx, provided its connectors stay within your data policies. A Power BI report suits analysis rather than editing, once you have checked where the data is hosted. A PCF control is the right bet only when the interaction must live on the form, nothing native exists, and the deadline leaves time for specification, dependency review and testing.
Can Solzet just review our existing PCF development process?
Yes. We are often asked to audit rather than to build: whether the manifest exposes the right configuration, whether work sits in the wrong lifecycle method, whether the dataset control handles paging honestly, whether destroy actually releases what init created, and whether the solution and versioning process gives you a rollback position. That produces a written picture of what you have and the smallest change that fixes it, which is a much cheaper starting point than a rebuild and is frequently what we recommend.
Why do our custom PCF controls break on every Dynamics 365 or Power Apps update?
Because they depend on something outside the framework contract. The usual causes are code that reaches into the form markup beyond the control container, APIs used without a feature-usage declaration in the manifest or driven through the Xrm global, and React, Fluent UI or charting libraries pinned to old versions that drift away from what the host now provides. The host implementation changes with each release wave while the contract stays stable, so controls that stay inside the contract rarely notice an update and controls that reach outside it notice every one.
The developer who built our PCF controls has left. Should we harden, replace or rebuild them?
Decide per control after a triage of its manifest, dependencies and API usage, plus Solution Checker over the solutions that carry it. Harden when the source builds and the unsupported patterns are few and local. Replace with an out-of-the-box control when the modern grid, the editable grid or another first party control now covers most of the behaviour, which is often the cheapest answer. Rebuild when there is no usable source, when the design depends on host markup, or when hardening would touch most of the code anyway.
How does a mostly admin team keep taken-over PCF controls working after each release wave?
With a handover standard rather than more developers. Keep every control in source control you own, build and package it through a pipeline instead of on one machine, and run a documented upgrade test in a sandbox with early access switched on before each release wave, signed off by a named person. Keep a register of which forms each control sits on, and make sure an administrator knows how to set a field back to its default control so a failing control can be taken out of the user path without a developer.
Which related services fit alongside this?
Hire a Dynamics 365 Developer or CRM Developer in Armenia
Hire a senior Dynamics 365 developer or CRM developer from Solzet in Yerevan, Armenia: Dataverse, C# plugins, PCF controls, integrations and project rescue.
Power Platform Consulting and Development Services
Power Platform consulting and development by senior Solzet developers: Power Apps, Power Automate and RPA, Power Pages, Copilot Studio, Dataverse and PCF.
Dynamics 365 Subcontracting & White-Label Services for Microsoft Partners
White-label Dynamics 365 subcontracting for Microsoft partners. You keep the client and SOW; we deliver CE, Power Platform and PCF from Armenia. NDA-protected.
Ready to start?
Contact Solzet for a free consultation. A senior consultant replies within one business day with a recommendation, whether that is Dynamics 365, Power Platform, or a custom-built CRM.