Development

Building web resources with React in Dynamics 365

June 15, 2025
Sabhat Qureshi
11 min read

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.

Need help with your Dynamics 365 project?

I can help you apply these practices in your own Dynamics 365 environment.