Introduction
Out-of-the-box Power Apps controls are functional. They get the job done for simple forms and basic UI. But when a business analyst asks for a Gantt chart, an interactive org chart, a drag-and-drop kanban board, or an address autocomplete that matches your corporate style — you hit the wall.

The Power Apps Component Framework (PCF) is the escape hatch. It lets you write custom controls in TypeScript that run natively inside both Canvas and Model-Driven apps. You get the full power of modern web development — React, D3.js, Web APIs — while staying inside the Power Platform ecosystem.
This guide walks through building a production-ready PCF component from scratch, covering the gotchas that documentation glosses over.
Prerequisites and Tooling Setup
Before writing a single line of PCF code, you need a proper development environment:
# PowerShell: PCF Development Environment Setup
# 1. Install Node.js LTS (required for PCF CLI)
# Download from https://nodejs.org - use LTS version (18.x or 20.x)
node --version # Should show v18.x or v20.x
# 2. Install Power Platform CLI
# Option A: Via npm (cross-platform)
npm install -g pac-cli
# Option B: Via dotnet tool (if .NET SDK installed)
dotnet tool install --global Microsoft.PowerApps.CLI.Tool
# Verify installation
pac --version
# 3. Install Visual Studio Code extensions
# - ESLint
# - Prettier
# - Power Platform Tools (official Microsoft extension)
# 4. Create your PCF project
mkdir MyCustomControls
Set-Location MyCustomControls
# Initialize a PCF control project
# Type: field (for single-value controls) or dataset (for collections)
pac pcf init --namespace "Contoso.Controls" `
--name "InteractiveKanban" `
--template "dataset" `
--framework "react" `
--run-npm-install
Write-Host "PCF project initialized. Open in VS Code:" -ForegroundColor Green
Write-Host "code ." -ForegroundColor Cyan
Expected output:
added 245 packages in 8s
found 0 vulnerabilities
Understanding PCF Architecture
Architecture Overview: Power Apps Runtime

Building a Production PCF Component
The ControlManifest.Input.xml
This is the contract between your component and Power Apps:
<?xml version="1.0" encoding="utf-8"?>
<manifest>
<control namespace="Contoso.Controls"
constructor="InteractiveKanban"
version="1.0.0"
display-name-key="InteractiveKanban"
description-key="Drag-and-drop kanban board for task management"
control-type="standard"
api-version="1.3.4">
<!-- Bound dataset - the collection of records -->
<data-set name="kanbanItems"
display-name-key="Items"
description-key="The dataset of items to display on the kanban board"
cds-data-set-options="displayCommandBar:true;displayViewSelector:true">
<property-set name="title"
display-name-key="Title Column"
description-key="Column containing the item title"
of-type="SingleLine.Text"
usage="bound"
required="true" />
<property-set name="status"
display-name-key="Status Column"
description-key="Column containing the status (determines kanban column)"
of-type="SingleLine.Text"
usage="bound"
required="true" />
<property-set name="priority"
display-name-key="Priority Column"
description-key="Column for priority indicator"
of-type="Whole.None"
usage="bound"
required="false" />
</data-set>
<!-- Configuration properties -->
<property name="columnConfig"
display-name-key="Column Configuration"
description-key="JSON config for kanban columns"
of-type="Multiple"
usage="input"
required="true" />
<property name="cardColor"
display-name-key="Card Color"
description-key="Default card background color"
of-type="SingleLine.Text"
usage="input"
required="false"
default-value="#ffffff" />
<resources>
<code path="index.ts" order="1" />
<platform-library name="React" version="16.8.6" />
<platform-library name="Fluent" version="9.46.2" />
<css path="css/kanban.css" order="1" />
<resx path="strings/InteractiveKanban.1033.resx" order="1" />
</resources>
</control>
</manifest>
The Main Component Class (index.ts)
// index.ts - PCF Component entry point
import { IInputs, IOutputs } from "./generated/ManifestTypes";
import * as React from "react";
import * as ReactDOM from "react-dom";
import { KanbanBoard, IKanbanProps } from "./KanbanBoard";
export class InteractiveKanban
implements ComponentFramework.StandardControl<IInputs, IOutputs>
{
private container: HTMLDivElement;
private context: ComponentFramework.Context<IInputs>;
private notifyOutputChanged: () => void;
private currentItems: IKanbanItem[];
public init(
context: ComponentFramework.Context<IInputs>,
notifyOutputChanged: () => void,
state: ComponentFramework.Dictionary,
container: HTMLDivElement
): void {
this.container = container;
this.context = context;
this.notifyOutputChanged = notifyOutputChanged;
this.currentItems = [];
// Enable full-screen mode support
context.mode.trackContainerResize(true);
}
public updateView(
context: ComponentFramework.Context<IInputs>
): void {
this.context = context;
const dataset = context.parameters.kanbanItems;
// Wait for data to load
if (dataset.loading) return;
// Transform dataset records into kanban items
this.currentItems = this.transformRecords(dataset);
// Parse column configuration
const columnConfig = this.parseColumnConfig(
context.parameters.columnConfig?.raw || "[]"
);
const props: IKanbanProps = {
items: this.currentItems,
columns: columnConfig,
cardColor: context.parameters.cardColor?.raw || "#ffffff",
onItemMoved: this.handleItemMoved.bind(this),
onItemClicked: this.handleItemClicked.bind(this),
width: context.mode.allocatedWidth,
height: context.mode.allocatedHeight,
};
ReactDOM.render(
React.createElement(KanbanBoard, props),
this.container
);
}
private transformRecords(
dataset: ComponentFramework.PropertyTypes.DataSet
): IKanbanItem[] {
return dataset.sortedRecordIds.map((id) => {
const record = dataset.records[id];
return {
id: id,
title: record.getValue("title")?.toString() || "",
status: record.getValue("status")?.toString() || "",
priority: Number(record.getValue("priority")) || 0,
};
});
}
private async handleItemMoved(
itemId: string,
newStatus: string
): Promise<void> {
// Update the record in Dataverse
const dataset = this.context.parameters.kanbanItems;
const record = dataset.records[itemId];
if (record) {
// Use webAPI to update the record
const entityRef = record.getNamedReference();
await this.context.webAPI.updateRecord(
entityRef.entityType!,
entityRef.id!,
{ statusfield: newStatus }
);
// Refresh the dataset
dataset.refresh();
}
}
private handleItemClicked(itemId: string): void {
const record =
this.context.parameters.kanbanItems.records[itemId];
if (record) {
const entityRef = record.getNamedReference();
this.context.navigation.openForm({
entityId: entityRef.id!,
entityName: entityRef.entityType!,
});
}
}
public destroy(): void {
ReactDOM.unmountComponentAtNode(this.container);
}
}

Testing and Debugging
Local Test Harness
# PowerShell: Run the PCF test harness locally
Set-Location "C:\Projects\MyCustomControls"
# Start the test harness with hot reload
npm start watch
# This opens a browser at http://localhost:8181
# You can:
# - Resize the container to test responsive behavior
# - Edit input properties in real-time
# - Debug with browser DevTools
# - Set breakpoints in TypeScript source maps
# Build for production
npm run build
# Run linting
npm run lint
Automated Testing Strategy
{
"testing_strategy": {
"unit_tests": {
"framework": "Jest + React Testing Library",
"coverage_target": "80%",
"test_areas": [
"Data transformation logic",
"Column configuration parsing",
"Drag-and-drop state management",
"Edge cases: empty dataset, single item, 1000+ items"
]
},
"integration_tests": {
"framework": "PCF Test Harness + Playwright",
"test_areas": [
"Rendering with mock dataset",
"Drag-and-drop interaction",
"WebAPI update calls",
"Form navigation on click"
]
},
"performance_benchmarks": {
"targets": {
"initial_render_100_items": "< 200ms",
"initial_render_1000_items": "< 500ms",
"drag_drop_response": "< 50ms",
"memory_usage_1000_items": "< 50MB"
}
}
}
}
Packaging and Deployment
# PowerShell: Package and deploy PCF component to Dataverse
# 1. Create a solution project
pac solution init --publisher-name "Contoso" `
--publisher-prefix "con" `
--outputDirectory "ContosoControlsSolution"
Set-Location ContosoControlsSolution
# 2. Add the PCF project reference
pac solution add-reference --path "..\MyCustomControls"
# 3. Build the solution (creates .zip for import)
dotnet build --configuration Release
# Output: bin\Release\ContosoControlsSolution.zip
# 4. Deploy to environment
pac auth create --environment "https://yourorg.crm.dynamics.com"
pac solution import --path "bin\Release\ContosoControlsSolution.zip" `
--activate-plugins
Write-Host "Solution deployed! Component available in app maker." -ForegroundColor Green
Write-Host ""
Write-Host "To use in Canvas App:" -ForegroundColor Cyan
Write-Host " 1. Settings > Components > Import components"
Write-Host " 2. Select 'Code' tab"
Write-Host " 3. Find 'InteractiveKanban' and import"
Write-Host " 4. Add to screen from Insert > Code components"
Expected output:
Build succeeded.
0 Warning(s)
0 Error(s)
Time Elapsed 00:00:04.12

Common Pitfalls and Solutions
| Pitfall | Symptom | Solution |
|---|---|---|
| Bundle too large | Slow loading, timeout errors | Tree-shake dependencies, lazy load heavy libraries |
| Memory leaks | App slows over time | Clean up event listeners in destroy(), unmount React |
| Dataset pagination | Only 25 records show | Implement dataset.paging.loadNextPage() |
| Responsive failure | Component doesn't resize | Use trackContainerResize(true) + CSS flexbox |
| Missing types | Build errors after update | Regenerate types: npm run refreshTypes |
| CORS errors in test | API calls fail locally | Use mock data in test harness, test real in environment |
Architecture Decision and Tradeoffs
When designing low-code development solutions with Power Apps, consider these key architectural trade-offs:
| Approach | Best For | Tradeoff |
|---|---|---|
| Managed / platform service | Rapid delivery, reduced ops burden | Less customisation, potential vendor lock-in |
| Custom / self-hosted | Full control, advanced tuning | Higher operational overhead and cost |
Recommendation: Start with the managed approach for most workloads and move to custom only when specific requirements demand it.
Validation and Versioning
- Last validated: April 2026
- Validate examples against your tenant, region, and SKU constraints before production rollout.
- Keep module, CLI, and SDK versions pinned in automation pipelines and review quarterly.
Security and Governance Considerations
- Apply least-privilege access using RBAC roles and just-in-time elevation for admin tasks.
- Store secrets in managed secret stores and avoid embedding credentials in scripts or source files.
- Enable audit logging, data protection policies, and periodic access reviews for regulated workloads.
Cost and Performance Notes
- Define budgets and alerts, then monitor usage and cost trends continuously after go-live.
- Baseline performance with synthetic and real-user checks before and after major changes.
- Scale resources with measured thresholds and revisit sizing after usage pattern changes.
Official Microsoft References
- https://learn.microsoft.com/power-apps/
- https://learn.microsoft.com/power-platform/admin/
- https://learn.microsoft.com/power-platform/guidance/
Public Examples from Official Sources
- These examples are sourced from official public Microsoft documentation and sample repositories.
- Documentation examples: https://learn.microsoft.com/power-apps/
- Sample repositories: https://github.com/microsoft/PowerApps-Samples
- Prefer adapting these examples to your tenant, subscriptions, and governance requirements before production use.
Key Takeaways
- PCF components unlock custom UI capabilities that standard Power Apps controls cannot provide — kanban boards, Gantt charts, org charts, custom maps
- Use the React framework template for complex UI components — it provides component lifecycle management and efficient re-rendering
- The ControlManifest defines the contract — design it carefully because changing it later requires a new version
- Always implement the
destroy()method to prevent memory leaks — unmount React components and remove event listeners - Use the local test harness for rapid development, but always test in a real Power Apps environment before deployment
- Performance matters — test with realistic data volumes (100, 500, 1000+ records) and set benchmarks
- Package components in Dataverse solutions for proper ALM — never deploy components manually to production
