you’re sitting at your desk with a user story that you’re trying to break down into a solution. The objective is simple, you need to display dataverse data in a way that looks pretty enough to impress your boss for their next meeting with the command team. Luckily for you, you have spent a better part of this decade honing your skills in Power Apps as a “low-code” developer. That being said, you quickly realize the out-of-the-box canvas app won’t cut it. Your boss needs a dashboard that pulls data from multiple Dataverse tables—think contacts, accounts, and custom activity records—all joined together to show meaningful insights. But Dataverse struggles with complex multi-join queries, and you can’t efficiently reshape the data on the fly. You consider moving everything to Power BI for proper analysis and visualization, but that means your boss can’t interact with the data directly in the app, update records, or drill down into details without switching tools. You’re stuck between Dataverse’s great business logic and authorization features, and its limitations when it comes to reporting and complex data relationships. The solution you envisioned is starting to look a lot more complicated than you hoped.
So you’ve got a few paths forward: a React PCF, a Dataverse plugin, or, my personal favorite at the moment, a well integrated Next.js embed on a canvas app. PCF exists in a locked down world. Power Apps and Dynamics 365 need security, performance, and integration with low code platforms more than they need developer freedom. Coming from full stack development (Next.js, especially), PCF’s lifecycle methods, init(), updateView(), and destroy(), feel restrictive. They lack the flexibility you get with Next.js plugins or server actions when you’re trying to extend an app. Let me walk through the problems with init, updateView, and destroy, and why I lean toward Next.js based plugins when I need real extensibility.
The problem with init() in PCF
No async support
The method doesn’t support async/await, and the framework won’t wait for promises to resolve. If you make an API call in init()init(), updateView() may be called before the data returns. You must manually manage state and re-render on data arrival, which breaks intuitive flow.
public init(context: Context, notifyOutputChanged, state, container) {
// This won't pause execution, updateView runs immediately after
this.fetchData().then(data => {
this.data = data;
this.render(context); // Manual render needed
});
}
In a Next.js plugin, you can use async components, server actions or even a hybrid of the two. It’s straightforward, you await your data, then return your component and in my opinion its easier to read with my 4 bad eyes:
async function MyPlugin() {
const data = await fetch('<https://api.example.com/data>');
const json = await data.json();
return <div>{json.items.map(item => ...)}</div>;
}
Async server components and server actions
Next.js lets your page component be async and fetch data directly before rendering, which in an internal environment you won’t care for the SEO benefits but you should appreciate the lack of refreshes and valid HTML on first paint. You can also define server actions, functions marked with ‘use server’, in the same file or import them separately. These handle form submissions or mutations without the ceremony of setting up separate API routes.
'use server'
// Server action defined at the top
async function updateItem(formData: FormData) {
// Handle mutation
const id = formData.get('id');
// Update logic here
}
// Async component that fetches data
export default async function Page() {
const data = await fetch('<https://api.example.com/data>');
const json = await data.json();
return (
<div>
{json.items.map(item => <div key={item.id}>{item.name}</div>)}
<form action={updateItem}>
<button type="submit">Update</button>
</form>
</div>
);
}
This gives you the best of both: async data fetching for initial render and server actions for user interactions, all in one file. No manual state management, no worrying about race conditions between lifecycle methods.
Full async support, SSR, caching, and middleware. No need to worry about race conditions with UI lifecycle.
I jumped the gun a bit on updates. Lets go over updateView() my personal enemy.
No control over rendering timing
The problem with updateView() is you can’t debounce or batch updates. Reacting to every change causes performance issues, especially in dataset controls with frequent sorting or filtering. You’re at the mercy of the framework’s update cycle, which can feel like trying to control a fire hydrant with your bare hands. That little devil called ‘convenience’ strikes again.
Contrast with Next.js
In Next.js, you control rendering via React state, useEffect, or useQuery. You can debounce inputs, cache responses, and optimize re-renders naturally. It’s straightforward, you have full control over data fetching, caching, and UI updates without fighting against the lifecycle.
//Doesn't this look pretty?
const { data, isLoading } = useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
refetchOnWindowFocus: false,
});
Before I go over the destructive behavior of destroy(), I should note something that I have been thinking about as I try to become more proficient in Dataverse plugins and PCFs. When I open my Visual Studio Code, I enter the blue accented abyss knowing that I am about to code. Knowing that, wouldn’t I want to use the tool that makes my capability to accomplish the task as simple as possible? The tool that gives me the swiss army knife over the butter knife? They’re both knives, but one is a lot sharper and can tighten a screw if you ever need it along the way. What recourse does one have but to reach for the sharper blade when the dull one requires twice the effort for half the result?
Now let’s talk about the third devil in PCF’s lifecycle: destroy(). It’s meant for DOM cleanup, like unmounting React components with ReactDOM.unmountComponentAtNode, but that’s where its usefulness ends. There’s no support for async cleanup, resource disposal, or persistent state management. If your component uses subscriptions or web sockets, you’re on your own for teardown, and there’s no guarantee even gets called in all scenarios. It’s a Swiss cheese safety net.destroy()
public destroy(): void {
// Synchronous cleanup only - no async support
if (this.subscription) {
this.subscription.unsubscribe(); // Hope this completes in time
}
// Can't await cleanup of resources
// this.closeConnection() might return a promise, but you can't wait for it
this.closeConnection(); // Fires and forgets
// Unmount React component
ReactDOM.unmountComponentAtNode(this.container);
// No guarantee this even runs in all scenarios
}
In Next.js, you can use API routes with proper request/response lifecycle management. Middleware handles authentication, logging, and cleanup without the guesswork. Server-side logic runs in a full Node.js environment, giving you proper resource management instead of hoping the framework remembers to call your cleanup function. It’s the difference between being handed a mop and being given an industrial cleaning crew.
// API route with PROPER lifecycle management
export async function GET(request: Request) {
// Middleware handles auth, logging, etc. automatically
const connection = await openDatabaseConnection();
try {
const data = await connection.query('SELECT * FROM items');
return Response.json(data);
} finally {
// Proper async cleanup - guaranteed to run
await connection.close();
// Resources are disposed properly
// No guesswork, no hoping the framework calls cleanup
}
}
// Component cleanup with useEffect
export default function MyComponent() {
useEffect(() => {
const subscription = subscribeToData();
// Return cleanup function - runs on unmount
return () => {
subscription.unsubscribe();
// Full control over when and how cleanup happens
};
}, []);
return <div>Content</div>;
}
What React PCF gets right (and wrong)
React PCFs run directly in Model-Driven and Canvas Apps, binding seamlessly to form fields, datasets, and context. Once you build one, you can deploy it across multiple apps and environments. You get Fluent UI support for consistent Microsoft design, and the dev stack uses TypeScript, React, and npm, which most frontend developers already know. For simple UI extensions inside forms, PCFs do their job.
But Canvas Apps only support React 16. Model-Driven Apps support React 17. You’re missing modern features like the use hook or server actions. The init() method doesn’t support async/await, so you can’t wait for data fetching, leading to race conditions. The updateView() method fires excessively, even on unrelated changes, forcing constant re-renders you can’t control. And destroy() offers minimal cleanup with no server-side logic. You’re stuck in a lifecycle that feels like trying to parallel park a semi-truck in a compact space.
What a Next.js embed brings to the table
A Next.js app embedded in Power Apps gives you full M365 API access. You can securely integrate Dataverse, Microsoft Graph, Power BI, and Azure Key Vault via API routes. You get modern full-stack capabilities with NextAuth.js, server actions, SSR, and middleware. You can run it on Azure with CI/CD, monitoring, and isolated secrets via Key Vault. Embed it with an iframe in Canvas or Model-Driven Apps, or via Power Pages. Heavy logic, caching, and data enrichment all happen server-side, improving performance and keeping your client lean.
The tradeoffs? It’s not a “native” component, so you need iframe or portal embedding. Authentication requires Azure AD app registration and token management. External hosting might introduce load delays. You’ll have separate deployment, monitoring, and security policies to manage. If you’re just a power app dev it can feel like more governance overhead, but if you expand your hat to M365 Dev, you’re still well within your comfort zone to create solutions.
When to use which
Use React PCF for UI extensions inside forms. If you need direct form context access, real-time data binding, or automatic SSO with M365, PCFs handle that natively. But if you need server-side logic, complex data queries, or modern React features, you’re better off with Next.js. PCFs are acrylics, precise and native but limited by the frame. Next.js is digital art, richer textures, animation, depth, hung on the same canvas via iframe. Why choose even? You can be greedy, you can have both if you have time to mix it all together.
Canvas Apps are a blank canvas. It doesn’t care if the art was made with React, Next.js, or Azure Functions. It only shows the final picture. What users see, the experience, is what counts. So whether you build with PCFs or embedded apps, focus on what you’re creating, not just the tools you’re using.
Leave a Reply