Documentation

Best Practices

Modern Power Apps development with declarative formulas, named formulas, and minimal imperative code

Formulas First

Use declarative formulas instead of imperative code for better performance and maintainability

Code Quality

Write readable, maintainable PowerFx with consistent patterns

Security

Implement proper authentication, validation, and data protection

Accessibility

Make apps usable for everyone with proper labels and contrast

Version Control

Track changes, manage deployments, and maintain rollback capability

Documentation

Document complex logic, component usage, and deployment procedures

The Modern Approach: Declarative Over Imperative

Modern Power Apps development has shifted from imperative (OnStart, Set, ClearCollect) to declarative (formulas, named formulas). This approach is:

  • Faster - No blocking OnStart operations
  • Cleaner - Less code, more readable
  • Maintainable - Logic is centralized in named formulas
  • Reactive - Automatically updates when source data changes
// ❌ Old Imperative Pattern
OnStart = 
    ClearCollect(colUsers, Users);
    Set(varActiveUsers, Filter(colUsers, Status = "Active"));
    Set(varUserCount, CountRows(varActiveUsers));

// ✅ Modern Declarative Pattern  
ActiveUsers = Filter(Users, Status = "Active")
UserCount = CountRows(ActiveUsers)

// Use directly - no OnStart, no Set, no collections
Gallery.Items = ActiveUsers
Label.Text = "Total: " & UserCount

Key Principle: Only use imperative code (Set, UpdateContext, ClearCollect) when formulas can't solve the problem.

Component Architecture

Keep Components Focused

Each component should handle one specific responsibility. Avoid monolithic screens that mix data logic, UI, and business rules.

// ❌ Bad - Everything in one place
Set(varUser, LookUp(Users, Email = User().Email));
Set(varPermissions, LookUp(Permissions, UserID = varUser.ID));
Set(varData, Filter(Records, CreatedBy = varUser.ID));
Patch(Analytics, Defaults(Analytics), {Action: "PageView"});
// ✅ Good - Separated concerns
// OnVisible
Run(InitializeUserContext);
Run(LoadPageData);
Run(TrackAnalytics);

Use Named Patterns

Give components clear, descriptive names that indicate their purpose and type.

// Naming Convention
[Type][Purpose][Variant]

Examples:
- btnSubmitPrimary
- galProductsList
- lblErrorMessage
- conUserDetailsCard
- icnNavigationBack

Container Hierarchy

Organize complex layouts with clear container hierarchy:

Screen
└── conMainLayout
    ├── conHeader
    │   ├── conNavigation
    │   └── conUserProfile
    ├── conContent
    │   ├── conSidebar
    │   └── conMainArea
    └── conFooter

Modern Power Apps: Formulas First

Use Formulas Instead of OnStart

Modern Power Apps development favors declarative formulas over imperative code. Formulas are evaluated on-demand, don't block loading, and are easier to maintain.

// ❌ Old Pattern - Imperative OnStart
OnStart = 
    ClearCollect(colUsers, Users);
    ClearCollect(colProducts, Products);
    Set(varFilteredProducts, Filter(Products, Category = "Active"));

Gallery.Items = colFilteredProducts
// ✅ Modern Pattern - Direct Formulas
// No OnStart needed!
Gallery.Items = Filter(Products, Category = "Active")

// Power Apps evaluates this automatically when Products changes

Named Formulas for Reusable Logic

Create app-level named formulas for logic you use in multiple places.

// Define once in Named Formulas
ActiveProducts = Filter(Products, Status = "Active")
CurrentUserRole = LookUp(UserRoles, Email = User().Email).Role
IsAdmin = CurrentUserRole = "Admin"

// Use anywhere in your app
Gallery1.Items = ActiveProducts
Gallery2.Items = ActiveProducts
btnAdminPanel.Visible = IsAdmin

Benefits:

  • ✅ No OnStart or Set() needed
  • ✅ Updates automatically when source data changes
  • ✅ Reusable across screens
  • ✅ Easier to test and debug
  • ✅ Better performance (evaluated on-demand)

Avoid Unnecessary Collections

Only use collections when you need to cache data or modify it locally. Direct formulas are usually better.

// ❌ Old Pattern - Unnecessary collection
Screen.OnVisible = 
    ClearCollect(colFiltered, Filter(Products, Status = "Active"));

Gallery.Items = colFiltered
// ✅ Modern Pattern - Direct formula
Gallery.Items = Filter(Products, Status = "Active")

// Even better - Named Formula
ActiveProducts = Filter(Products, Status = "Active")
Gallery.Items = ActiveProducts

When to use collections:

  • ✅ Caching data that won't change (like lookup tables)
  • ✅ Building temporary data structures for complex operations
  • ✅ Offline scenarios where you need local data
  • ✅ Modifying data before submission (draft records)

When NOT to use collections:

  • ❌ Simple filtering or sorting (use formulas)
  • ❌ Data that's already in a data source (just reference it)
  • ❌ Calculations that can be done with formulas

Delegation Warnings

Always address delegation warnings. Non-delegable queries are limited to 2,000 rows.

// ❌ Bad - Non-delegable (StartsWith not delegable in many connectors)
Filter(LargeTable, StartsWith(Name, "A"))

// ✅ Good - Delegable alternative
Filter(LargeTable, 'Name' >= "A" && 'Name' < "B")

// Or use Search when supported
Search(LargeTable, "searchterm", "Name")

Limit Concurrent Calls

Use sequential loading or batching for multiple data sources.

// ❌ Bad - All fire at once
OnVisible = 
    ClearCollect(colA, SourceA);
    ClearCollect(colB, SourceB);
    ClearCollect(colC, SourceC);
    ClearCollect(colD, SourceD);

// ✅ Good - Concurrent with control
Concurrent(
    ClearCollect(colA, SourceA),
    ClearCollect(colB, SourceB)
);
Concurrent(
    ClearCollect(colC, SourceC),
    ClearCollect(colD, SourceD)
);

State Management

Formulas > Variables

Modern Power Apps favor formulas over variables. Use variables only when necessary.

// ❌ Old Pattern - Variable for everything
OnStart = Set(varUserEmail, User().Email)
Label.Text = varUserEmail

// ✅ Modern Pattern - Direct formula
Label.Text = User().Email

// ✅ Even Better - Named Formula (if used multiple times)
CurrentUserEmail = User().Email
Label.Text = CurrentUserEmail

When to Use Variables

Variables still have their place, but use them sparingly:

Context Variables (UpdateContext) - Use for:

  • UI state toggles (editing mode, expanded/collapsed)
  • Temporary form state
  • Screen-specific flags
// Good use of context variable
UpdateContext({locIsEditing: true})

Toggle.OnChange = UpdateContext({locShowDetails: Self.Value})
Container.Visible = locShowDetails

Global Variables (Set) - Use for:

  • User session data that's expensive to recalculate
  • App configuration loaded once
  • Feature flags
// Good use of global variable - set once, use many times
OnStart = Set(varEnvironment, 
    If(Param("env") = "prod", "Production", "Development")
)

Named Formulas - Use for:

  • Calculated values used in multiple places
  • Complex logic that needs a name
  • Anything that changes with source data
// Named Formulas - the modern replacement for most variables
FilteredOrders = Filter(Orders, Status = "Pending")
TotalRevenue = Sum(FilteredOrders, Amount)
UserPermissions = LookUp(Permissions, UserID = User().Email)
CanEdit = UserPermissions.Role in ["Admin", "Editor"]

Avoid Variables When Formulas Work

Don't create variables for things that can be formulas.

// ❌ Old Pattern - Unnecessary variables
Set(varTotal, Sum(Orders, Amount));
Set(varAverage, Average(Orders, Amount));
Set(varCount, CountRows(Orders));

Label1.Text = varTotal
Label2.Text = varAverage
Label3.Text = varCount
// ✅ Modern Pattern - Direct formulas
Label1.Text = Sum(Orders, Amount)
Label2.Text = Average(Orders, Amount)
Label3.Text = CountRows(Orders)

// ✅ Even Better - Named formulas (if used multiple times)
OrderTotal = Sum(Orders, Amount)
OrderAverage = Average(Orders, Amount)
OrderCount = CountRows(Orders)

With() for Complex Calculations

Use With() to avoid repeating expensive operations within a single formula.

// ❌ Bad - Repeated lookups (old pattern)
Set(varUser, LookUp(Users, ID = varUserID));
Label1.Text = varUser.FirstName
Label2.Text = varUser.LastName
Label3.Text = varUser.Email

// ✅ Good - With() for inline scoping (modern pattern)
With(
    {currentUser: LookUp(Users, ID = Gallery.Selected.UserID)},
    Concatenate(
        currentUser.FirstName, " ",
        currentUser.LastName, " <",
        currentUser.Email, ">"
    )
)

// ✅ Best - Named Formula for reuse
CurrentUser = LookUp(Users, ID = Gallery.Selected.UserID)
Label1.Text = CurrentUser.FirstName
Label2.Text = CurrentUser.LastName
Label3.Text = CurrentUser.Email

Code Quality

Format Formulas for Readability

Use consistent indentation and line breaks in complex formulas.

// ❌ Bad - One long line
If(varStatus="Active"&&varRole="Admin"||varRole="Owner",Notify("Access granted"),If(varStatus="Pending",Notify("Approval required"),Notify("Access denied")))
// ✅ Good - Readable structure
If(
    varStatus = "Active" && (varRole = "Admin" || varRole = "Owner"),
    Notify("Access granted", NotificationType.Success),
    If(
        varStatus = "Pending",
        Notify("Approval required", NotificationType.Warning),
        Notify("Access denied", NotificationType.Error)
    )
)

// ✅ Even Better - Named Formula with clear logic
HasActiveAccess = varStatus = "Active" && varRole in ["Admin", "Owner"]
IsPending = varStatus = "Pending"

If(
    HasActiveAccess,
    Notify("Access granted", NotificationType.Success),
    IsPending,
    Notify("Approval required", NotificationType.Warning),
    Notify("Access denied", NotificationType.Error)
)

Break Down Complex Logic

Split complex formulas into named formulas for clarity.

// ❌ Bad - Complex inline formula
Gallery.Items = Filter(
    Products,
    (Category = "Electronics" || Category = "Computers") &&
    Price >= 100 && Price <= 1000 &&
    (InStock = true || PreOrder = true) &&
    (Brand = "Dell" || Brand = "HP" || Brand = "Lenovo")
)
// ✅ Good - Named formulas for clarity
TechCategories = ["Electronics", "Computers"]
PriceRange = Price >= 100 && Price <= 1000
IsAvailable = InStock || PreOrder
PreferredBrands = ["Dell", "HP", "Lenovo"]

FilteredProducts = Filter(
    Products,
    Category in TechCategories &&
    PriceRange &&
    IsAvailable &&
    Brand in PreferredBrands
)

Gallery.Items = FilteredProducts

Error Handling

Graceful Degradation

Always handle potential failures.

// ❌ Bad - No error handling
Button.OnSelect = 
    Patch(DataSource, Defaults(DataSource), FormData);
    Navigate(SuccessScreen);

// ✅ Good - With error handling
Button.OnSelect = 
    If(
        !IsBlank(Patch(DataSource, Defaults(DataSource), FormData)),
        Navigate(SuccessScreen),
        Notify("Failed to save record. Please try again.", NotificationType.Error)
    );

User Feedback

Provide clear, actionable error messages.

// ❌ Bad - Generic error
"Error occurred"

// ✅ Good - Specific and actionable
"Unable to save changes. Check your connection and try again."

// ✅ Better - With context
"Failed to update order #" & varOrderID & ". Your changes were not saved."

Quick Reference

Modern Patterns

  • • Named formulas for reusable logic
  • • Direct formula bindings
  • • Filter/Search directly on data sources
  • • With() for complex inline calculations
  • • Handle delegation warnings
  • • Validate user input
  • • Provide error messages
  • • Use meaningful names
  • • Add accessibility labels
  • • Test with empty data
  • • Document complex logic
  • • Version control solutions

Legacy Patterns

  • • OnStart for data loading
  • • Set() for calculated values
  • • ClearCollect for simple filtering
  • • Variables for formulas
  • • Hardcode credentials
  • • Ignore delegation limits
  • • Create unnecessary collections
  • • Skip error handling
  • • Use generic error messages
  • • Forget accessibility
  • • Deploy without testing
  • • Modify production directly

Quick Decision Trees

"Should I use a variable or formula?"

  1. Does the value change based on source data? → Named Formula
  2. Is it a UI state toggle? → Context Variable
  3. Is it set once at app start? → Global Variable (rare!)
  4. Everything else → Named Formula

"Should I use a collection?"

  1. Caching expensive lookups? → Maybe
  2. Offline data? → Yes
  3. Simple filtering? → No - use formula
  4. Temporary modifications? → Yes

Note: These practices are based on real-world Power Apps development experience. Adapt them to your team's needs and organizational standards.