Powerapps

Building Custom PCF Components for Power Apps: From Zero to Production

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.

Introduction

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

Terminal output for npm install

Understanding PCF Architecture

Architecture Overview: Power Apps Runtime

PCF Architecture

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

Building Components

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

Terminal output for dotnet build

Deployment

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

Public Examples from Official Sources

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

Key Takeaways

Additional Resources

AI Assistant
AI Assistant

Article Assistant

Ask me about this article

AI
Hi! I'm here to help you understand this article. Ask me anything about the content, concepts, or implementation details.