Why React for Dynamics 365 web resources?
Web resources in Dynamics 365 allow you to embed custom HTML, JavaScript, and CSS content within model-driven apps. While traditional JavaScript works well for simple scenarios, complex user interfaces benefit tremendously from React's component-based architecture, state management, and ecosystem. Web resources in Dynamics 365.
Benefits of using React
- Component reusability: Build once, use across multiple forms and apps
- State management: Predictable data flow with hooks and context
- Rich ecosystem: Thousands of ready-to-use components and libraries
- Developer experience: Hot reload, TypeScript support, excellent tooling
- Performance: Virtual DOM efficiently updates only changed elements
Project setup
Creating the React project
Set up a new React project configured for Dynamics 365 deployment:
# Create React app with TypeScript npx create-react-app d365-webresource --template typescript cd d365-webresource # Install additional dependencies npm install @types/xrm axios # For UI components (optional) npm install @fluentui/react
TypeScript configuration for Xrm
Add Xrm type definitions for Dynamics 365 API access:
// tsconfig.json
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"types": ["xrm"]
},
"include": ["src"]
}Build configuration for single file output
Configure the build to output a single JavaScript file suitable for web resource deployment:
// For Create React App, use react-app-rewired
npm install react-app-rewired
// config-overrides.js
module.exports = {
webpack: function(config, env) {
// Disable code splitting
config.optimization.splitChunks = {
cacheGroups: {
default: false
}
};
config.optimization.runtimeChunk = false;
// Single output file
config.output.filename = 'static/js/bundle.js';
return config;
}
};
// Update package.json scripts
"scripts": {
"start": "react-app-rewired start",
"build": "react-app-rewired build",
"test": "react-app-rewired test"
}Accessing Dynamics 365 context
Getting parent context
Web resources run in an iframe, so you need to access the parent Xrm context:
// src/utils/XrmContext.ts
declare global {
interface Window {
parent: Window & { Xrm?: typeof Xrm };
}
}
export function getXrm(): typeof Xrm | undefined {
// Try parent window first (when embedded in form)
if (window.parent && window.parent.Xrm) {
return window.parent.Xrm;
}
// Fall back to current window (for standalone testing)
if ((window as any).Xrm) {
return (window as any).Xrm;
}
return undefined;
}
export function getFormContext(): Xrm.FormContext | undefined {
const xrm = getXrm();
if (xrm && xrm.Page) {
return xrm.Page as unknown as Xrm.FormContext;
}
return undefined;
}React context provider for Xrm
Create a React context to provide Xrm access throughout your component tree:
// src/context/D365Context.tsx
import React, { createContext, useContext, useEffect, useState } from 'react';
import { getXrm, getFormContext } from '../utils/XrmContext';
interface D365ContextType {
xrm: typeof Xrm | undefined;
formContext: Xrm.FormContext | undefined;
recordId: string | undefined;
entityName: string | undefined;
isLoading: boolean;
}
const D365Context = createContext<D365ContextType>({
xrm: undefined,
formContext: undefined,
recordId: undefined,
entityName: undefined,
isLoading: true
});
export const D365Provider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [state, setState] = useState<D365ContextType>({
xrm: undefined,
formContext: undefined,
recordId: undefined,
entityName: undefined,
isLoading: true
});
useEffect(() => {
const initContext = () => {
const xrm = getXrm();
const formContext = getFormContext();
if (xrm) {
setState({
xrm,
formContext,
recordId: formContext?.data?.entity?.getId()?.replace(/[{}]/g, ''),
entityName: formContext?.data?.entity?.getEntityName(),
isLoading: false
});
} else {
setTimeout(initContext, 100);
}
};
initContext();
}, []);
return (
<D365Context.Provider value={state}>
{children}
</D365Context.Provider>
);
};
export const useD365 = () => useContext(D365Context);Web API integration
Custom hook for Web API calls
Create a custom hook for making Web API calls with proper authentication:
// src/hooks/useWebApi.ts
import { useCallback } from 'react';
import { useD365 } from '../context/D365Context';
export function useWebApi() {
const { xrm } = useD365();
const retrieveRecord = useCallback(async (
entityName: string,
recordId: string,
options?: string
) => {
if (!xrm) throw new Error('Xrm not available');
return await xrm.WebApi.retrieveRecord(entityName, recordId, options);
}, [xrm]);
const retrieveMultiple = useCallback(async (
entityName: string,
options?: string
) => {
if (!xrm) throw new Error('Xrm not available');
return await xrm.WebApi.retrieveMultipleRecords(entityName, options);
}, [xrm]);
const createRecord = useCallback(async (
entityName: string,
data: object
) => {
if (!xrm) throw new Error('Xrm not available');
return await xrm.WebApi.createRecord(entityName, data);
}, [xrm]);
const updateRecord = useCallback(async (
entityName: string,
recordId: string,
data: object
) => {
if (!xrm) throw new Error('Xrm not available');
return await xrm.WebApi.updateRecord(entityName, recordId, data);
}, [xrm]);
const deleteRecord = useCallback(async (
entityName: string,
recordId: string
) => {
if (!xrm) throw new Error('Xrm not available');
return await xrm.WebApi.deleteRecord(entityName, recordId);
}, [xrm]);
return { retrieveRecord, retrieveMultiple, createRecord, updateRecord, deleteRecord };
}Building components
Example: Related records component
Build a component that displays related records with editing capabilities:
// src/components/RelatedContacts.tsx
import React, { useEffect, useState } from 'react';
import { useD365 } from '../context/D365Context';
import { useWebApi } from '../hooks/useWebApi';
interface Contact {
contactid: string;
fullname: string;
emailaddress1: string;
telephone1: string;
}
export const RelatedContacts: React.FC = () => {
const { recordId, entityName, isLoading: contextLoading } = useD365();
const { retrieveMultiple } = useWebApi();
const [contacts, setContacts] = useState<Contact[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchContacts = async () => {
if (!recordId || entityName !== 'account') {
setLoading(false);
return;
}
try {
const result = await retrieveMultiple(
'contacts',
`?$select=contactid,fullname,emailaddress1,telephone1&$filter=_parentcustomerid_value eq ${recordId}`
);
setContacts(result.entities as Contact[]);
} finally {
setLoading(false);
}
};
if (!contextLoading) fetchContacts();
}, [recordId, entityName, contextLoading, retrieveMultiple]);
if (contextLoading || loading) return <div>Loading...</div>;
return (
<div className="contacts-grid">
<h3>Related Contacts ({contacts.length})</h3>
<table>
<thead>
<tr><th>Name</th><th>Email</th><th>Phone</th></tr>
</thead>
<tbody>
{contacts.map(contact => (
<tr key={contact.contactid}>
<td>{contact.fullname}</td>
<td>{contact.emailaddress1}</td>
<td>{contact.telephone1}</td>
</tr>
))}
</tbody>
</table>
</div>
);
};Using Fluent UI components
Fluent UI provides components that match the Dynamics 365 look and feel:
// Using Fluent UI for consistent D365 styling
import { Card, CardHeader, Text, Badge, makeStyles } from '@fluentui/react-components';
import { PersonRegular, MailRegular } from '@fluentui/react-icons';
const useStyles = makeStyles({
card: {
marginBottom: '8px',
cursor: 'pointer',
'&:hover': { boxShadow: '0 2px 8px rgba(0,0,0,0.15)' }
}
});
export const ContactCard: React.FC<ContactCardProps> = ({ contactId, fullname, email, isPrimary }) => {
const styles = useStyles();
const { xrm } = useD365();
const openContact = () => {
xrm?.Navigation.openForm({ entityName: 'contact', entityId: contactId });
};
return (
<Card className={styles.card} onClick={openContact}>
<CardHeader
header={
<div>
<PersonRegular />
<Text weight="semibold">{fullname}</Text>
{isPrimary && <Badge>Primary</Badge>}
</div>
}
/>
</Card>
);
};Communication with parent form
Reading and writing form data
Interact with form fields from your React web resource:
// src/hooks/useFormAttribute.ts
import { useState, useEffect, useCallback } from 'react';
import { useD365 } from '../context/D365Context';
export function useFormAttribute<T>(attributeName: string) {
const { formContext } = useD365();
const [value, setValue] = useState<T | null>(null);
useEffect(() => {
if (!formContext) return;
const attribute = formContext.getAttribute(attributeName);
if (attribute) {
setValue(attribute.getValue() as T);
attribute.addOnChange(() => setValue(attribute.getValue() as T));
}
}, [formContext, attributeName]);
const updateValue = useCallback((newValue: T) => {
if (!formContext) return;
const attribute = formContext.getAttribute(attributeName);
if (attribute) {
attribute.setValue(newValue);
setValue(newValue);
}
}, [formContext, attributeName]);
return [value, updateValue] as const;
}
// Usage
const [accountName, setAccountName] = useFormAttribute<string>('name');
const [revenue, setRevenue] = useFormAttribute<number>('revenue');Deployment
Building for production
# Build the production bundle npm run build # Output structure: # build/ # index.html → Upload as HTML WebResource # static/js/bundle.js → Upload as JS WebResource # static/css/main.css → Upload as CSS WebResource
HTML web resource template
Create an HTML file that loads your React bundle:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>React WebResource</title>
<link href="new_/css/main.css" rel="stylesheet">
<style>
body { margin: 0; padding: 8px; font-family: 'Segoe UI', sans-serif; }
#root { min-height: 100vh; }
</style>
</head>
<body>
<div id="root"></div>
<script src="new_/js/bundle.js"></script>
</body>
</html>Adding to form
- Open the form editor in make.powerapps.com
- Add a new section or tab for the web resource
- Insert HTML web resource component
- Select your uploaded HTML file
- Configure row span and column span as needed
- Check "Pass record object-type code and unique identifier as parameters" if needed
Best practices
Performance
- Keep bundle size small; use code splitting where possible
- Implement loading states for async operations
- Debounce form field updates to avoid excessive API calls
- Use React.memo for expensive components
Error handling
- Use error boundaries to catch and display errors gracefully
- Handle cases where Xrm context is not available (testing scenarios)
- Log errors to application insights or custom logging
Development workflow
- Use mock data during local development
- Set up environment variables for different D365 environments
- Automate web resource deployment with CLI tools
- Include web resources in your solution for ALM
Conclusion
Building web resources with React brings modern web development practices to Dynamics 365 customization. The combination of React's component model, TypeScript's type safety, and Dynamics 365's extensibility creates powerful custom experiences that enhance user productivity. Start with simple components and gradually build more complex solutions as you become comfortable with the integration patterns.