Meta Description: Learn how to build a custom Power Apps Component Framework (PCF) Todo List control using React, TypeScript, and Fluent UI. Complete step-by-step guide with code examples, deployment instructions, and best practices.
Keywords: PCF development, Power Apps Component Framework, React PCF control, TypeScript PCF, Fluent UI, Power Apps custom control, PCF tutorial, Power Apps development, custom control development
Reading Time: ~25 minutes | Difficulty: Intermediate | Last Updated: October 2025
Quick Summary
This comprehensive tutorial will teach you how to build a fully functional Power Apps Component Framework (PCF) Todo List control from scratch. By the end of this guide, you'll have:
✅ Built a custom PCF control using React and TypeScript
✅ Integrated with Power Apps data using datasets and context
✅ Implemented CRUD operations for todo management
✅ Added interactive features like selection and navigation
✅ Deployed your control to a Power Apps environment
What You'll Learn:
- PCF architecture and development fundamentals
- React component development for Power Apps
- Data binding and context management
- Fluent UI integration and responsive design
- Solution packaging and deployment strategies
Prerequisites: Basic knowledge of React, TypeScript, and Power Apps concepts.
Table of Contents
- Understanding PCF Fundamentals
- Creating the Starter Template
- Building UI with Static Data
- Adding Dynamic Data with Context
- Implementing Record Navigation
- Adding Task Completion Functionality
- Implementing Todo Selection
- Deployment and Testing
- Frequently Asked Questions (FAQ)
- Conclusion
Understanding PCF Fundamentals
What is Power Apps Component Framework (PCF)?
Power Apps Component Framework (PCF) is Microsoft's official development framework for creating custom controls in Power Apps, Power Pages, and model-driven applications. PCF enables developers to build sophisticated, interactive components using modern web technologies including React, TypeScript, and CSS.
PCF serves as a crucial bridge between Power Apps' no-code/low-code environment and traditional web development, allowing developers to extend platform capabilities with custom functionality that isn't available out-of-the-box.
Key Benefits of PCF Development:
- Enhanced User Experience: Create custom UI components tailored to specific business needs
- Modern Web Technologies: Leverage React, TypeScript, and modern CSS frameworks
- Seamless Integration: Native integration with Power Apps data and APIs
- Reusability: Build once, deploy across multiple Power Apps environments
- Performance: Optimized rendering and data handling for enterprise applications
💡 Pro Tip: If you're new to Power Apps development, check out our Power Apps fundamentals guide before diving into PCF controls. Understanding the platform basics will accelerate your PCF learning curve.
Key PCF Concepts
Understanding these core concepts is essential for successful PCF development:
-
Manifest File (
ControlManifest.Input.xml):- Defines control metadata, properties, and capabilities
- Specifies data sources and external service requirements
- Configures control behavior and appearance settings
-
Context Object:
- Provides access to Power Apps data and APIs
- Enables communication between your control and the Power Apps environment
- Contains user information, device capabilities, and utility functions
-
Dataset:
- Represents entity records bound to the control
- Provides methods for data manipulation and record selection
- Handles data refresh and filtering operations
-
Properties:
- Configurable inputs that app makers can set
- Enable customization without code changes
- Support various data types (text, numbers, booleans, etc.)
-
Lifecycle Methods:
init(): Control initialization and setupupdateView(): Renders the control UIgetOutputs(): Returns control output valuesdestroy(): Cleanup when control is removed
🔗 Related: We'll implement these lifecycle methods in detail in the Building UI with Static Data section.
PCF Architecture
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Power Apps │ │ PCF Control │ │ React UI │
│ Environment │◄──►│ (index.ts) │◄──►│ Components │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│
▼
┌─────────────────┐
│ Context API │
│ (Data Access) │
└─────────────────┘
Creating the Starter Template
Prerequisites for PCF Development
Before building your first PCF control, ensure you have the following development environment set up:
Required Software:
- Node.js (v14 or later) – JavaScript runtime for building and testing
- npm or yarn – Package manager for dependencies
- Power Apps CLI – Microsoft's official CLI for PCF development
- Visual Studio Code (recommended) – IDE with excellent TypeScript and React support
Optional but Recommended:
- Git – Version control for your PCF projects
- Power Apps Developer Plan – Free environment for testing your controls
- Fluent UI React – Microsoft's design system components
Step 1: Install Power Apps CLI
The Power Apps CLI is the essential tool for PCF development. Install it globally to access PCF commands from any directory:
npm install -g @microsoft/powerapps-cli
What this command does:
- Installs the Power Apps CLI globally on your system
- Enables PCF project creation, building, and testing commands
- Provides access to solution packaging and deployment tools
- Includes the test harness for local development and debugging
Step 2: Create a New PCF Project
Create your first PCF control project using the Power Apps CLI. This command sets up a complete development environment:
pac pcf init --namespace rtn --name TodoList --template dataset --framework react
Command Parameters Explained:
--namespace rtn: Sets your organization's namespace prefix--name TodoList: Defines the control name (becomesrtn.TodoList)--template dataset: Creates a data-bound control template--framework react: Sets up React with TypeScript support
Project Structure Created:
- TypeScript configuration files
- React component templates
- Manifest file for control definition
- Build and test scripts
- Fluent UI integration setup
📁 File Structure: The generated project includes all necessary files for PCF development. We'll explore the key files in the Building UI with Static Data section.
Step 3: Install Dependencies
npm install
Step 4: Install Additional Dependencies
For our Todo List control, we only need to install Tailwind CSS:
npm install tailwindcss@^4.1.11 @tailwindcss/cli@^4.1.11
The React framework and Fluent UI components are already included when using the --framework react flag, so we only need to add the styling framework. It's like getting a starter kit that actually includes the essentials for once.
Building UI with Static Data
Step 1: Create the Todo Model
Create TodoList/models/todo.model.ts:
export interface Todo {
readonly id: string;
readonly name: string;
readonly description: string;
isCompleted: boolean;
}
// Static data for initial development
export const staticTodos: Todo[] = [
{
id: "1",
name: "Learn PCF Development",
description:
"Understand the fundamentals of Power Apps Component Framework",
isCompleted: false,
},
{
id: "2",
name: "Build Todo Control",
description: "Create a custom todo list control using React and TypeScript",
isCompleted: true,
},
{
id: "3",
name: "Deploy to Power Apps",
description: "Package and deploy the control to Power Apps environment",
isCompleted: false,
},
];
Step 2: Create the Todo Card Component
Create TodoList/components/todo-card.tsx:
import * as React from "react";
import {
Card,
CardFooter,
CardHeader,
Subtitle1,
} from "@fluentui/react-components";
import { Todo } from "../models/todo.model";
interface ITodoCard {
todo: Todo;
}
export const TodoCard = ({ todo }: ITodoCard) => {
return (
<Card className="!flex-row gap-2 items-start">
<div className="flex-grow flex flex-col gap-2">
<CardHeader header={<Subtitle1>{todo.name}</Subtitle1>} />
<CardFooter>{todo.description}</CardFooter>
</div>
</Card>
);
};
Code Explanation:
- Imports: We import React and Fluent UI components for the card layout, plus our Todo model
- Interface:
ITodoCarddefines the props this component expects (just atodoobject) - Component: The
TodoCardfunction receives atodoprop and renders it using Fluent UI components - Layout: Uses a
Cardwith flexbox layout (!flex-row gap-2 items-start) for horizontal arrangement - Content: Displays the todo name in the header and description in the footer
- Styling: Uses Tailwind classes for responsive layout and spacing
This component displays a basic todo item with just the name and description. We'll add more functionality in later steps.
Step 3: Create the Main Todo List Component
Create TodoList/rtn-todo-list.tsx:
import * as React from "react";
import { TodoCard } from "./components/todo-card";
import { FluentProvider, webLightTheme } from "@fluentui/react-components";
import { staticTodos } from "./models/todo.model";
import { Todo } from "./models/todo.model";
export const RtnTodoList = () => {
const [todos, setTodos] = React.useState<Todo[]>(staticTodos);
return (
<FluentProvider theme={webLightTheme}>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 !p-4">
{todos.map((todo) => (
<TodoCard key={todo.id} todo={todo} />
))}
</div>
</FluentProvider>
);
};
Code Explanation:
- Imports: React for state management, TodoCard component, Fluent UI provider and theme, plus our static data and Todo type
- State:
useStatehook manages the todos array, initialized with our static data - Provider:
FluentProviderwraps the entire component to provide Fluent UI theming - Grid Layout: Uses Tailwind's responsive grid system:
grid-cols-1: 1 column on mobilemd:grid-cols-2: 2 columns on medium screenslg:grid-cols-3: 3 columns on large screens
- Mapping:
todos.map()renders each todo as a TodoCard component - Key Prop: Each TodoCard gets a unique
keyprop (todo.id) for React's reconciliation
We're using a responsive grid layout here because apparently mobile-first design is still a thing. The component manages the state for todos and renders them in a nice grid.
Step 4: Update the Main Control Class
Update TodoList/index.ts:
import { IInputs, IOutputs } from "./generated/ManifestTypes";
import { RtnTodoList } from "./rtn-todo-list";
import * as React from "react";
export class TodoList
implements ComponentFramework.ReactControl<IInputs, IOutputs>
{
private notifyOutputChanged: () => void;
constructor() {
// Empty constructor
}
public init(
context: ComponentFramework.Context<IInputs>,
notifyOutputChanged: () => void,
state: ComponentFramework.Dictionary
): void {
this.notifyOutputChanged = notifyOutputChanged;
}
public updateView(
context: ComponentFramework.Context<IInputs>
): React.ReactElement {
return React.createElement(RtnTodoList, {});
}
public getOutputs(): IOutputs {
return {};
}
public destroy(): void {
// Add code to cleanup control if necessary
}
}
Code Explanation:
- Imports: TypeScript interfaces for inputs/outputs, our React component, and React itself
- Class Declaration:
TodoListimplements the PCFReactControlinterface with typed inputs and outputs - Constructor: Empty constructor (PCF handles initialization)
- init(): Called when the control is first created:
- Stores the
notifyOutputChangedcallback for later use - Context contains all the data and APIs from Power Apps
- Stores the
- updateView(): Called whenever the control needs to re-render:
- Creates and returns our React component
- This is where the magic happens – PCF renders our React UI
- getOutputs(): Returns any output values (empty for now)
- destroy(): Cleanup method called when control is removed
This is the main control class that Power Apps will instantiate. It's like the conductor of an orchestra, but the orchestra is your React components.
Step 5: Set Up Tailwind CSS
Create TodoList/styles/input.css:
@import "tailwindcss";
Code Explanation:
- @import: Imports all Tailwind CSS utilities and components
- Simple Setup: This single line gives us access to all Tailwind classes
Update package.json to include CSS build script:
{
"scripts": {
"watch:css": "tailwindcss -i ./TodoList/styles/input.css -o ./TodoList/styles/output.css --watch"
}
}
Code Explanation:
- Script Name:
watch:css– a custom npm script for CSS compilation - Input File:
-i ./TodoList/styles/input.css– reads from our input CSS file - Output File:
-o ./TodoList/styles/output.css– writes compiled CSS to output file - Watch Mode:
--watch– automatically recompiles when input changes
Tailwind CSS is like having a utility belt for styling. Instead of writing custom CSS, you just add classes to your HTML elements.
Step 6: Update the Manifest to Include CSS
Update TodoList/ControlManifest.Input.xml to include the CSS resource:
<resources>
<code path="index.ts" order="1"/>
<platform-library name="React" version="16.14.0" />
<platform-library name="Fluent" version="9.46.2" />
<css path="styles/output.css" order="1" />
</resources>
Code Explanation:
- resources: Container for all files the control needs to load
- code: Points to our main TypeScript entry point (
index.ts) - platform-library: Pre-installed libraries (React and Fluent UI)
- css: Our compiled Tailwind CSS file (
styles/output.css) - order: Loading priority (1 = highest priority)
This tells Power Apps to include our compiled CSS file when the control loads.
Step 7: Test the Static UI
npm run start
This will start the test harness where you can see your control with static data.
Adding Dynamic Data with Context
Step 1: Update the Manifest
Update TodoList/ControlManifest.Input.xml to include property definitions:
<?xml version="1.0" encoding="utf-8" ?>
<manifest>
<control namespace="rtn" constructor="TodoList" version="1.0.0" display-name-key="TodoList" description-key="TodoList description" control-type="virtual">
<external-service-usage enabled="false">
</external-service-usage>
<data-set name="dataSet" display-name-key="DataSet">
</data-set>
<property name="name_logicalname" display-name-key="Name Logical Name" description-key="Todo item name" of-type="SingleLine.Text" usage="input" required="true" />
<property name="description_logicalname" display-name-key="Description Logical Name" description-key="Todo item description" of-type="SingleLine.Text" usage="input" required="true" />
<property name="iscompleted_logicalname" display-name-key="Is Completed Logical Name" description-key="Whether the todo item is completed" of-type="SingleLine.Text" usage="input" required="true" />
<property name="entity_logicalname" display-name-key="Entity Logical Name" description-key="The logical name of the entity" of-type="SingleLine.Text" usage="input" required="true" />
<resources>
<code path="index.ts" order="1"/>
<platform-library name="React" version="16.14.0" />
<platform-library name="Fluent" version="9.46.2" />
<css path="styles/output.css" order="1" />
</resources>
<feature-usage>
<uses-feature name="WebAPI" required="true" />
</feature-usage>
</control>
</manifest>
Code Explanation:
- control: Main control definition with namespace, constructor name, and version
- data-set: Defines a dataset that will contain entity records from Power Apps
- property: Input parameters that users configure in Power Apps:
name_logicalname: Field name for todo titledescription_logicalname: Field name for todo descriptioniscompleted_logicalname: Field name for completion statusentity_logicalname: The entity type (e.g., "rtn_todo")
- feature-usage: Declares we need WebAPI access for CRUD operations
This XML file is like a contract between your control and Power Apps. It tells Power Apps what your control can do and what data it needs.
Step 2: Generate TypeScript Types
npm run refreshTypes
This generates TodoList/generated/ManifestTypes.d.ts with proper TypeScript interfaces.
Step 3: Update the Todo Model for Dynamic Data
Update TodoList/models/todo.model.ts:
import type { IInputs } from "../generated/ManifestTypes";
export interface Todo {
readonly id: string;
readonly name: string;
readonly description: string;
isCompleted: boolean;
}
export function createTodoFromRecord(
record: ComponentFramework.PropertyHelper.DataSetApi.EntityRecord,
inputs: IInputs
): Todo {
return {
id: record.getRecordId(),
name: record.getValue(inputs.name_logicalname.raw!) as string,
description: record.getValue(inputs.description_logicalname.raw!) as string,
isCompleted: record.getValue(inputs.iscompleted_logicalname.raw!) === "1",
};
}
Code Explanation:
- Import: Gets TypeScript interfaces from generated manifest types
- Todo Interface: Defines our data structure with readonly properties (except
isCompleted) - createTodoFromRecord Function: Converts Power Apps records to Todo objects:
record.getRecordId(): Gets the unique ID from the Power Apps recordrecord.getValue(): Retrieves field values using logical names from inputsinputs.name_logicalname.raw!: Gets the configured field name (e.g., "rtn_name")=== "1": Converts Power Apps boolean (1/0) to JavaScript boolean
This function converts Power Apps records into our Todo objects. It's like a translator between Power Apps data and our React components.
Step 4: Update the Todo Card Component for Dynamic Data
Update TodoList/components/todo-card.tsx to work with dynamic data:
import * as React from "react";
import {
Card,
CardFooter,
CardHeader,
Subtitle1,
Button,
} from "@fluentui/react-components";
import { createTodoFromRecord } from "../models/todo.model";
import { CheckmarkFilled, DismissFilled } from "@fluentui/react-icons";
import type { IInputs } from "../generated/ManifestTypes";
interface ITodoCard {
context: ComponentFramework.Context<IInputs>;
record: ComponentFramework.PropertyHelper.DataSetApi.EntityRecord;
}
export const TodoCard = ({ record, context }: ITodoCard) => {
const todo = createTodoFromRecord(record, context.parameters);
const [todoState, setTodoState] = React.useState(todo);
return (
<Card className="!flex-row gap-2 items-start">
<div className="flex-grow flex flex-col gap-2">
<CardHeader
action={
<Button
icon={
todoState.isCompleted ? (
<CheckmarkFilled color="green" />
) : (
<DismissFilled color="red" />
)
}
onClick={() => {}} // Placeholder for now
/>
}
header={<Subtitle1>{todo.name}</Subtitle1>}
/>
<CardFooter>{todo.description}</CardFooter>
</div>
</Card>
);
};
Code Explanation:
- Interface Update: Now receives
contextandrecordinstead of a statictodoobject - Data Conversion:
createTodoFromRecord()converts Power Apps record to Todo object - State Management:
useStatetracks the todo state for UI updates - Dynamic Icons: Shows green checkmark for completed, red X for incomplete todos
- Placeholder Action:
onClick={() => {}}is a placeholder for completion functionality - Context Access:
context.parametersprovides access to all configured properties
The component now works with real Power Apps data instead of static objects.
Step 5: Update the Main Component to Use Context
Update TodoList/rtn-todo-list.tsx:
import * as React from "react";
import { TodoCard } from "./components/todo-card";
import { FluentProvider, webLightTheme } from "@fluentui/react-components";
import type { IInputs } from "./generated/ManifestTypes";
export interface IRtnTodoList {
context: ComponentFramework.Context<IInputs>;
}
export const RtnTodoList = ({ context }: IRtnTodoList) => {
const sortedIds = context.parameters.dataSet.sortedRecordIds;
const records = sortedIds.map((id) => context.parameters.dataSet.records[id]);
return (
<FluentProvider theme={webLightTheme}>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 !p-4">
{records.map((record) => (
<TodoCard
key={record.getRecordId()}
record={record}
context={context}
/>
))}
</div>
</FluentProvider>
);
};
Code Explanation:
- Interface:
IRtnTodoListdefines that the component receives acontextprop - Data Access:
context.parameters.dataSetprovides access to Power Apps records - Sorted Records:
sortedRecordIdsgives us records in the order Power Apps wants them displayed - Record Mapping:
sortedIds.map()converts IDs to actual record objects - Props Passing: Each
TodoCardreceives therecordandcontextfor data access - No State: Removed local state since we're now using Power Apps data directly
The component now renders real data from Power Apps instead of static todos. The context object gives us access to the dataset and all the records.
Step 6: Update the Control Class
Update TodoList/index.ts:
import { IInputs, IOutputs } from "./generated/ManifestTypes";
import { RtnTodoList, IRtnTodoList } from "./rtn-todo-list";
import * as React from "react";
export class TodoList
implements ComponentFramework.ReactControl<IInputs, IOutputs>
{
private notifyOutputChanged: () => void;
constructor() {
// Empty
}
public init(
context: ComponentFramework.Context<IInputs>,
notifyOutputChanged: () => void,
state: ComponentFramework.Dictionary
): void {
this.notifyOutputChanged = notifyOutputChanged;
}
public updateView(
context: ComponentFramework.Context<IInputs>
): React.ReactElement {
const props: IRtnTodoList = {
context,
};
return React.createElement(RtnTodoList, props);
}
public getOutputs(): IOutputs {
return {};
}
public destroy(): void {
// Add code to cleanup control if necessary
}
}
Code Explanation:
- Import Update: Now imports
IRtnTodoListinterface for proper typing - Props Object: Creates a
propsobject with thecontextto pass to our React component - Context Passing: The
contextcontains all Power Apps data and APIs - React.createElement: Creates the React component with the context as props
- Type Safety: Uses TypeScript interfaces to ensure proper prop types
The control now passes the Power Apps context to our React component, giving it access to real data.
Implementing Record Navigation
Step 1: Update the Todo Card Component
Update TodoList/components/todo-card.tsx to include navigation functionality:
import * as React from "react";
import {
Card,
CardFooter,
CardHeader,
Subtitle1,
Button,
} from "@fluentui/react-components";
import { createTodoFromRecord } from "../models/todo.model";
import {
ArrowRightFilled,
CheckmarkFilled,
DismissFilled,
} from "@fluentui/react-icons";
import type { IInputs } from "../generated/ManifestTypes";
interface ITodoCard {
context: ComponentFramework.Context<IInputs>;
record: ComponentFramework.PropertyHelper.DataSetApi.EntityRecord;
}
export const TodoCard = ({ record, context }: ITodoCard) => {
const todo = createTodoFromRecord(record, context.parameters);
const [todoState, setTodoState] = React.useState(todo);
const goToDetailsPage = async () => {
await context.navigation.openForm({
entityName: context.parameters.entity_logicalname.raw!,
entityId: todoState.id,
});
};
return (
<Card className="!flex-row gap-2 items-start">
<div className="flex-grow flex flex-col gap-2">
<CardHeader
action={
<Button
icon={
todoState.isCompleted ? (
<CheckmarkFilled color="green" />
) : (
<DismissFilled color="red" />
)
}
onClick={() => {}} // Placeholder for now
/>
}
header={<Subtitle1>{todo.name}</Subtitle1>}
/>
<CardFooter
action={
<Button icon={<ArrowRightFilled />} onClick={goToDetailsPage} />
}
>
{todo.description}
</CardFooter>
</div>
</Card>
);
};
Code Explanation:
- Navigation Function:
goToDetailsPageis an async function that opens the record's detail form - context.navigation.openForm(): Power Apps API to open forms with parameters:
entityName: Uses the configured entity logical name from inputsentityId: Uses the current record's ID to open the specific record
- Navigation Button: Arrow button in
CardFootertriggers the navigation when clicked
The navigation functionality allows users to click the arrow button and open the full record details in Power Apps.
Adding Task Completion Functionality
Step 1: Implement Toggle Complete Function
Update TodoList/components/todo-card.tsx:
export const TodoCard = ({ record, context, selected }: ITodoCard) => {
const todo = createTodoFromRecord(record, context.parameters);
const [todoState, setTodoState] = React.useState(todo);
const toggleComplete = async () => {
const newValue = !todoState.isCompleted;
setTodoState((value) => ({ ...value, isCompleted: newValue }));
// Update the record in Power Apps
await context.webAPI.updateRecord(
context.parameters.entity_logicalname.raw!,
record.getRecordId(),
{
[context.parameters.iscompleted_logicalname.raw!]: newValue,
}
);
// Refresh the dataset to reflect changes (data doesn't update itself you know...)
context.parameters.dataSet.refresh();
};
return (
<Card className="!flex-row gap-2 items-start">
<div className="flex-grow flex flex-col gap-2">
<CardHeader
action={
<Button
icon={
todoState.isCompleted ? (
<CheckmarkFilled color="green" />
) : (
<DismissFilled color="red" />
)
}
onClick={toggleComplete}
/>
}
header={<Subtitle1>{todo.name}</Subtitle1>}
/>
<CardFooter
action={
<Button icon={<ArrowRightFilled />} onClick={goToDetailsPage} />
}
>
{todo.description}
</CardFooter>
</div>
</Card>
);
};
Code Explanation:
- Optimistic Update:
setTodoState()immediately updates the UI before server response for better user experience - WebAPI Call:
context.webAPI.updateRecord()updates the record in Power Apps - Entity Name: Uses configured entity logical name from inputs
- Record ID: Gets the specific record ID to update
- Field Update: Updates the completion field using the configured logical name
- Dataset Refresh:
context.parameters.dataSet.refresh()refreshes the data to reflect changes - Button Click:
onClick={toggleComplete}connects the button to the function
Key Features
- Immediate Feedback: Users see the change right away, which feels responsive
- Error Handling: If the update fails, you can revert the optimistic update
- Flexibility: The control works with any entity as long as the logical names are configured correctly
Implementing Todo Selection
Step 1: Update the Todo Card Component Interface
First, update the interface to include the selected prop:
interface ITodoCard {
context: ComponentFramework.Context<IInputs>;
record: ComponentFramework.PropertyHelper.DataSetApi.EntityRecord;
selected: boolean;
}
Code Explanation:
- selected: boolean: New prop that indicates whether this todo is currently selected
- Interface Update: Extends the existing interface to support selection state
Step 2: Add Selection Logic
Update TodoList/components/todo-card.tsx to include the selection functionality:
import {
Card,
CardFooter,
CardHeader,
Subtitle1,
Button,
Checkbox,
} from "@fluentui/react-components";
export const TodoCard = ({ record, context, selected }: ITodoCard) => {
const todo = createTodoFromRecord(record, context.parameters);
const [todoState, setTodoState] = React.useState(todo);
const handleSelect = (id: string) => {
const selectedIds = context.parameters.dataSet.getSelectedRecordIds();
if (selectedIds.includes(id)) {
context.parameters.dataSet.setSelectedRecordIds(
selectedIds.filter((selectedId) => selectedId !== id)
);
} else {
context.parameters.dataSet.setSelectedRecordIds([...selectedIds, id]);
}
};
return (
<Card className="!flex-row gap-2 items-start">
<Checkbox
checked={selected}
onChange={() => handleSelect(todoState.id)}
/>
<div className="flex-grow flex flex-col gap-2">
<CardHeader
action={
<Button
icon={
todoState.isCompleted ? (
<CheckmarkFilled color="green" />
) : (
<DismissFilled color="red" />
)
}
onClick={toggleComplete}
/>
}
header={<Subtitle1>{todo.name}</Subtitle1>}
/>
<CardFooter
action={
<Button icon={<ArrowRightFilled />} onClick={goToDetailsPage} />
}
>
{todo.description}
</CardFooter>
</div>
</Card>
);
};
Code Explanation:
- Checkbox Import: Added
Checkboxcomponent from Fluent UI - handleSelect Function: Manages selection state using Power Apps dataset API:
getSelectedRecordIds(): Gets currently selected record IDssetSelectedRecordIds(): Updates the selection state- Toggle logic: Adds ID if not selected, removes if already selected
- Checkbox Component: Renders a checkbox that reflects the selection state
- Layout Update: Checkbox is positioned at the start of the card layout
Step 3: Update the Main Component
Update TodoList/rtn-todo-list.tsx to pass the selected state:
export const RtnTodoList = ({ context }: IRtnTodoList) => {
const selectedIds = context.parameters.dataSet.getSelectedRecordIds();
const sortedIds = context.parameters.dataSet.sortedRecordIds;
const records = sortedIds.map((id) => context.parameters.dataSet.records[id]);
return (
<FluentProvider theme={webLightTheme}>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 !p-4">
{records.map((record) => (
<TodoCard
key={record.getRecordId()}
record={record}
context={context}
selected={selectedIds?.includes(record.getRecordId())}
/>
))}
</div>
</FluentProvider>
);
};
Code Explanation:
- selectedIds: Gets the array of currently selected record IDs from the dataset
- Selection Check:
selectedIds?.includes(record.getRecordId())checks if the current record is selected - Props Passing: Each
TodoCardreceives theselectedboolean prop based on selection state - Optional Chaining:
?.safely handles cases whereselectedIdsmight be undefined - Real-time Updates: Selection state updates automatically when users interact with checkboxes
This selection logic enables multi-select functionality with visual feedback through checkboxes that toggle the selection state.
Deployment and Testing
Step 1: Build the Control
npm run build
Code Explanation:
- Build Command: Compiles TypeScript, bundles React components, and processes CSS
- Output: Creates the control bundle in the
out/controls/TodoList/directory - Bundle Contents: Includes all JavaScript, CSS, and manifest files needed for deployment
This creates the control bundle in the out/controls/TodoList/ directory.
Step 2: Create Solution Package
Create a new folder for the solution and initialize it:
mkdir TodoListControl
cd TodoListControl
pac solution init --publisher-name "YourPublisher" --publisher-prefix "rtn"
pac solution add-reference --path ..
Code Explanation:
- mkdir: Creates a separate folder for the solution package
- cd: Navigates into the solution folder
- pac solution init: Initializes a new solution with publisher details
- pac solution add-reference: Links the solution to our PCF control project
- –path ..: References the parent directory where our control is located
Step 3: Build Solution
dotnet build
Code Explanation:
- dotnet build: Compiles the solution and creates the deployment package
- Output: Generates a
.zipfile in thebin/Debug/directory - Package Contents: Includes the PCF control and solution metadata for import into Power Apps
Step 4: Import to Power Apps
- Go to Power Apps maker portal
- Navigate to Apps → Solutions
- Import the generated solution file
- Add the control to a form or view
Step 5: Configure Control Properties
When adding the control to a form:
- DataSet: Bind to your todo entity
- Name Logical Name: Set to your name field (e.g.,
rtn_name) - Description Logical Name: Set to your description field (e.g.,
rtn_description) - Is Completed Logical Name: Set to your completion field (e.g.,
rtn_iscompleted) - Entity Logical Name: Set to your entity name (e.g.,
rtn_todo)
Best Practices and Tips
1. Error Handling
const toggleComplete = async () => {
try {
const newValue = !todoState.isCompleted;
setTodoState((value) => ({ ...value, isCompleted: newValue }));
await context.webAPI.updateRecord(
context.parameters.entity_logicalname.raw!,
record.getRecordId(),
{
[context.parameters.iscompleted_logicalname.raw!]: newValue,
}
);
context.parameters.dataSet.refresh();
} catch (error) {
console.error("Failed to update todo:", error);
// Revert optimistic update
setTodoState(todo);
}
};
Always handle errors gracefully. Your users will thank you.
2. Performance Optimization
- Use
React.memo()for components that don't need frequent re-renders - Implement proper loading states
- Use
useCallback()for event handlers
3. Accessibility
- Add proper ARIA labels
- Ensure keyboard navigation works
- Use semantic HTML elements
4. Testing
- Test with different data scenarios (because users will always find ways to break your code)
- Verify error handling
- Test on different screen sizes (because apparently not everyone uses a 27-inch monitor)
Frequently Asked Questions (FAQ)
What is PCF in Power Apps?
PCF (Power Apps Component Framework) is Microsoft's development framework for creating custom controls in Power Apps, Power Pages, and model-driven applications using modern web technologies like React and TypeScript.
Do I need to know React to build PCF controls?
While not strictly required, knowledge of React significantly enhances your PCF development capabilities. The framework supports React components and provides excellent TypeScript integration.
Can PCF controls work with external APIs?
Yes, PCF controls can integrate with external APIs through the WebAPI feature. You can make HTTP requests to external services and consume data from various sources.
How do I debug PCF controls?
Use the built-in test harness (npm run start) for local debugging. The Power Apps CLI provides comprehensive debugging tools and browser developer tools integration.
Are PCF controls responsive?
Yes, PCF controls are fully responsive and can adapt to different screen sizes. Use CSS frameworks like Tailwind CSS or Fluent UI for responsive design.
Can I use third-party libraries in PCF?
Yes, you can include third-party libraries, but they must be bundled with your control. Consider bundle size and licensing requirements when selecting libraries.
Conclusion
This comprehensive tutorial has guided you through building a complete PCF Todo List control from concept to deployment. You've mastered:
Core PCF Concepts:
- Understanding PCF architecture and lifecycle methods
- Working with manifest files and context objects
- Implementing data binding with datasets
Development Skills:
- Creating React components with TypeScript
- Integrating Fluent UI and Tailwind CSS
- Building responsive, accessible user interfaces
Advanced Features:
- Dynamic data integration with Power Apps
- CRUD operations and record navigation
- Multi-select functionality and state management
- Error handling and performance optimization
Deployment & Best Practices:
- Solution packaging and deployment
- Configuration management
- Testing and debugging strategies
Your PCF control is now production-ready and can be extended with advanced features like filtering, bulk operations, and custom theming.
Next Steps for Advanced PCF Development
Immediate Enhancements:
- Add filtering and search capabilities
- Implement bulk operations (delete, complete multiple)
- Add drag-and-drop reordering functionality
- Create custom themes and styling options
Advanced Features:
- Add unit tests with Jest and React Testing Library
- Implement offline support with data caching
- Add accessibility features (ARIA labels, keyboard navigation)
- Create reusable component libraries
Performance Optimization:
- Implement virtual scrolling for large datasets
- Add lazy loading for improved performance
- Optimize bundle size and loading times
- Add error boundaries and fallback UI
Integration Opportunities:
- Connect to external APIs and services
- Implement real-time data synchronization
- Add custom business logic and validation
- Create multi-tenant configurations
Ready to take your PCF development skills to the next level? Start with these enhancements and explore the vast possibilities of custom Power Apps controls!
Happy coding! 🚀
