Power Fx Patterns That Survive a Real Application
Declarative thinking, state discipline, error handling and component boundaries — the four things that decide whether an app stays maintainable.
Rajiv KumarJanuary 13, 20206 min readPower Fx is easy to write badly because it accepts procedural code. These are the patterns that hold up once an app has thirty screens and someone else has to change it: derive rather than store, handle errors where they happen, and give components real boundaries.
Power Fx will accept almost anything. You can write a canvas app entirely with variables and behaviour formulas, treating it as a scripting language, and it will work. Then it reaches thirty screens, someone else inherits it, and every change breaks something in a place they did not look.
The patterns below are what separate an app that stays workable from one that becomes a rewrite. None of them are advanced; all of them are habits.
1. Derive, do not store
The central discipline. Power Fx is declarative: a property formula re-evaluates when its inputs change. Every value you store in a variable instead of deriving is a value you have to keep correct by hand, in every place that could invalidate it.
// Procedural: three places must remember to update this.
Set(varSelectedTitle, Gallery1.Selected.Title);
// ...later, in three other handlers...
Set(varSelectedTitle, Gallery1.Selected.Title);
// Declarative: correct by construction, everywhere, always.
// Label.Text =
Gallery1.Selected.Title
The test to apply: can this value be computed from something already on screen? If yes, compute it. A variable is for something that genuinely cannot be derived — a mode flag, a step counter, a value from a previous screen.
Named formulas for shared derivations
When the same derivation is needed in several places, a named formula in App.Formulas is better than a variable set in OnStart. It is evaluated when used rather than at start-up, it cannot go stale, and it does not add to the time the app takes to open.
// App.Formulas — declarative, always current, costs nothing at start-up.
UserEmail = User().Email;
IsApprover = !IsBlank(
LookUp(Approvers, Email = UserEmail)
);
OpenRequests = Filter(Requests, Status = "Open");
Moving work out of OnStart into named formulas is frequently the single largest improvement available to an app's start-up time, and it removes state at the same time.
2. State discipline
Three kinds of state, three jobs, and a rule for each:
| Kind | Scope | Use it when |
|---|---|---|
Set | Whole app | Something genuinely app-wide that cannot be derived |
UpdateContext | One screen | Screen-local state — a dialog's visibility, an editing flag |
ClearCollect | Whole app | A working set, or data read many times in a session |
Prefer the narrowest scope that works. A global variable used on one screen is a global variable someone will have to trace across the whole app to change safely.
3. Handle errors where they happen
Most canvas apps assume writes succeed. They mostly do, and the exceptions — a permission changed, a validation rule fired, the network dropped mid-save — leave the user staring at a screen that looks like it worked.
// Every write can fail. Leave the user in a defined state when it does.
IfError(
Patch(
Requests,
Defaults(Requests),
{ Title: txtTitle.Text, Status: "Open" }
),
// Failure branch: say what happened, keep their input.
Notify(
"Could not save: " & FirstError.Message,
NotificationType.Error
),
// Success branch: only now is it safe to clear and move on.
Notify("Request created.", NotificationType.Success);
Navigate(scrList)
)
The important detail is the ordering: navigating away and clearing the form belong in the success branch. An app that clears the input and then discovers the save failed has lost the user's work.
4. Multi-step writes have no transaction
Canvas apps cannot write to two data sources atomically. Pretending otherwise is how partial records appear.
The pattern is: write the parent, capture the result, write children against it, check errors after each step, and define what happens on partial failure — usually either rolling back what succeeded, or marking the parent incomplete so a person can finish it. Choose deliberately; the default is silence.
Set(varParent, Patch(Orders, Defaults(Orders), { Customer: cmbCustomer.Selected }));
If(
IsBlank(varParent) || !IsBlank(Errors(Orders, varParent)),
Notify("Order not created; nothing was saved.", NotificationType.Error),
// Only now attempt the lines. A failure here leaves a parent with
// no lines, which is why it is marked rather than left ambiguous.
IfError(
ForAll(colLines, Patch(OrderLines, Defaults(OrderLines),
{ OrderId: varParent.ID, Sku: Sku, Qty: Qty })),
Patch(Orders, varParent, { Status: "Incomplete" });
Notify("Order saved but lines failed. Marked incomplete.", NotificationType.Warning)
)
)
5. Components need real boundaries
A component is worth making when the same thing appears in several places, and it is worth making properly when it has a defined interface.
Input properties in, output properties out. A component that reaches out to a global variable or a data source directly is not reusable — it is a screen fragment with extra steps, and it breaks the moment it is used somewhere the variable does not exist.
The discipline is the same as any other interface: if you cannot describe what goes in and what comes out in one sentence, the boundary is in the wrong place.
6. Performance is mostly about the network
In order of impact:
- Delegate. Filtering at the source is the difference between transferring a page and transferring a table — and between a correct answer and a wrong one.
- Load on demand. Move work out of
OnStart; a screen's data can load when the screen is shown. - Overlap independent calls with
Concurrent. - Reduce controls per screen. Control count affects render time directly.
- Only then look at formula complexity. It is rarely the bottleneck.
Measure with the monitor tool before changing anything. Optimising the formula that looks complicated is the most common wasted afternoon in canvas development.
7. Accessibility is not optional
Set AccessibleLabel on interactive controls, keep tab order sensible with TabIndex, do not encode meaning in colour alone, and check contrast. An internal application still has users who navigate by keyboard or screen reader, and retrofitting this is far more work than doing it as you go.
Common mistakes
- A long
OnStartsetting variables formulas could derive. The signature of a procedurally-written app. - Global variables for screen-local state. Every future change requires tracing the whole app.
- No error handling on writes. Silent data loss.
- Clearing the form before confirming the save. Loses the user's input on failure.
- Components that read globals. Not reusable, and fail in new contexts.
- Ignoring delegation warnings. Wrong results, not slow ones.
- Duplicated logic across screens. Named formulas and components exist for this.
Troubleshooting
| Symptom | Likely cause |
|---|---|
| App slow to open | Work in OnStart; move to named formulas and on-demand loading |
| A value is sometimes stale | Stored in a variable when it could have been derived |
| Save appears to work but nothing is written | No error handling; wrap in IfError and inspect Errors |
| Partial records in the data | Multi-step write with no failure path |
| Component behaves differently on a new screen | It depends on a global rather than an input property |
| Results differ between two environments | Different connector, and therefore different delegation support |
Frequently asked questions
Are named formulas always better than OnStart?
For deriving values, yes — they are current by construction and cost nothing at start-up. OnStart still has a job for genuine one-time work such as setting an initial screen or seeding a working collection.
When is a collection the right choice?
For a working set being assembled, and for data read many times in a session that does not need to be current. Not as a general cache, and not for large data — it lives in the app's memory.
How do I know whether something delegates?
The formula bar warns, and delegability is per connector. Test with more records than the row limit deliberately — that is the only way to see the failure, because below the limit everything looks correct.
Is Power Fx worth learning properly?
Yes, and mostly for one idea: declarative thinking. That single shift removes more state, more bugs and more maintenance than every other optimisation combined.
The habit underneath all of it
Before writing a formula, ask whether the value can be derived from what is already there. Most of the time it can, and the derived version cannot go stale, does not need updating in three places, and reads as what it is. That one question, asked consistently, is most of what separates a maintainable canvas app from an unmaintainable one.

