Delegates in React Native

Learn how to use delegates to receive events and customize behavior from the Agentforce SDK in your React Native application.

Overview

Delegates allow your JavaScript code to receive events and override behavior from the native Agentforce SDK. The bridge supports three delegate types:

Delegate Purpose Direction
Logger Receive SDK log messages Native → JS
Navigation Handle navigation requests from the agent Native → JS
View Provider Override native SDK views with React Native components JS → Native (registration) + Native → JS (rendering)

All delegates use a NativeEventEmitter on the JS side to receive events from the native layer.

Why Register Before configure()?

The recommended order is to register delegates before calling configure(). Here's why:

  • Logger: The SDK may emit log messages during initialization. If registered after configure(), those early messages are lost.
  • Navigation: The navigation bridge is passed to AgentforceConfiguration during configure(). Registering afterward still works because the bridge checks dynamically, but registering before is cleaner.
  • View Provider: Can be registered before or after configure() without issue. The native view provider checks the component map dynamically.

Logger Delegate

The Logger delegate forwards log messages from the native Agentforce SDK to your JavaScript code. This is useful for debugging, analytics, and monitoring.

Setup

import { AgentforceService } from "@salesforce/react-native-agentforce";

AgentforceService.setLoggerDelegate({
  onLog(level, message, error) {
    const prefix = `[Agentforce ${level.toUpperCase()}]`;
    if (error) {
      console.log(`${prefix} ${message} | Error: ${error}`);
    } else {
      console.log(`${prefix} ${message}`);
    }
  },
});

Parameters

Parameter Type Description
level 'error' | 'warn' | 'info' | 'debug' The severity level of the log message.
message string The log message from the SDK.
error string (optional) Stringified exception, present only for error/warn levels.

Clear the Delegate

AgentforceService.clearLoggerDelegate();

Platform Differences

Platform Log Levels Emitted
iOS error, warn, info, debug
Android error, warn, info

iOS emits the debug level; Android doesn't. Plan your log filtering accordingly.

Example: Analytics Forwarding

AgentforceService.setLoggerDelegate({
  onLog(level, message, error) {
    if (level === "error") {
      analytics.track("Agentforce Error", {
        message,
        error: error || undefined,
      });
    }
  },
});

Navigation Delegate

The Navigation delegate lets your app handle navigation requests from the Agentforce SDK. When the agent presents a link, record reference, or other navigable element that the user taps, the SDK emits a navigation event.

Setup

import { Linking } from "react-native";
import { AgentforceService } from "@salesforce/react-native-agentforce";

AgentforceService.setNavigationDelegate({
  onNavigate(request) {
    switch (request.type) {
      case "link":
        if (request.uri) {
          Linking.openURL(request.uri as string);
        }
        break;

      case "record":
        // Navigate to a record detail screen in your app
        navigation.navigate("RecordDetail", {
          recordId: request.recordId,
          objectType: request.objectType,
        });
        break;

      case "quickAction":
        console.log(`Quick action requested: ${request.actionName}`);
        break;

      case "objectHome":
        navigation.navigate("ObjectList", {
          objectType: request.objectType,
        });
        break;

      default:
        console.log("Unhandled navigation:", JSON.stringify(request));
    }
  },
});

Navigation Request Types

Type Description Fields
record Navigate to a Salesforce record recordId, objectType, pageReference?
link Open an external or internal URL uri, pageReference?
quickAction Execute a Salesforce quick action actionName, recordId?, objectType?
pageReference Navigate to a Lightning page reference pageReference
objectHome Navigate to an object's home/list page objectType, pageReference?
app Navigate to an app or external package packageName, uri?
unknown Catch-all for unrecognized types raw

Clear the Delegate

AgentforceService.clearNavigationDelegate();

View Provider Delegate

The View Provider delegate allows you to replace native SDK output views with custom React Native components. When the SDK renders a component type that matches your registered map, your React Native component is rendered instead.

Setup

There are two parts to registering a custom view:

Part 1: Register the React Native component with AppRegistry

import { AppRegistry } from "react-native";
import { ViewProviderComponentData } from "@salesforce/react-native-agentforce";

function CustomRichTextView({ componentData }: { componentData: ViewProviderComponentData }) {
  const text = componentData.properties.text as string;
  return (
    <View style={{ padding: 12 }}>
      <Text style={{ fontSize: 16 }}>{text}</Text>
    </View>
  );
}

// Register it with AppRegistry
AppRegistry.registerComponent("CustomRichTextView", () => CustomRichTextView);

Part 2: Register the component map with the bridge

await AgentforceService.setViewProviderDelegate({
  componentMap: {
    "copilot/richText": "CustomRichTextView",
    "copilot/markdown": "CustomMarkdownView",
  },
});

The keys are SDK component definition strings. The values are the names you registered with AppRegistry.registerComponent().

Complete Custom View Provider Example

This example shows a full custom view component that handles all properties and sub-components:

// CustomAgentforceView.tsx
import React from "react";
import { View, Text, ScrollView, StyleSheet } from "react-native";
import type { ViewProviderComponentData } from "@salesforce/react-native-agentforce";

interface Props {
  componentData: ViewProviderComponentData;
}

function CustomAgentforceView({ componentData }: Props) {
  const { definition, name, properties, subComponents } = componentData;

  return (
    <View style={styles.container}>
      <View style={styles.header}>
        <Text style={styles.defLabel}>{definition}</Text>
        {name && <Text style={styles.nameLabel}>{name}</Text>}
      </View>

      <ScrollView style={styles.propsContainer}>
        {Object.entries(properties).map(([key, value]) => (
          <View key={key} style={styles.propRow}>
            <Text style={styles.propKey}>{key}:</Text>
            <Text style={styles.propValue}>
              {typeof value === "object" ? JSON.stringify(value) : String(value)}
            </Text>
          </View>
        ))}
      </ScrollView>

      {subComponents && subComponents.length > 0 && (
        <View style={styles.subSection}>
          <Text style={styles.subTitle}>Sub-Components ({subComponents.length})</Text>
          {subComponents.map((sub, index) => (
            <View key={index} style={styles.subItem}>
              <Text style={styles.subDef}>{sub.definition}</Text>
              <Text style={styles.subProps}>{JSON.stringify(sub.properties, null, 2)}</Text>
            </View>
          ))}
        </View>
      )}
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    margin: 8,
    padding: 12,
    backgroundColor: "#f8f9fa",
    borderRadius: 8,
    borderWidth: 1,
    borderColor: "#dee2e6",
  },
  header: { marginBottom: 8 },
  defLabel: { fontSize: 14, fontWeight: "bold", color: "#0176D3" },
  nameLabel: { fontSize: 12, color: "#666", marginTop: 2 },
  propsContainer: { maxHeight: 200 },
  propRow: { flexDirection: "row", marginBottom: 4 },
  propKey: { fontWeight: "600", marginRight: 8, color: "#333" },
  propValue: { flex: 1, color: "#555" },
  subSection: { marginTop: 12, paddingTop: 8, borderTopWidth: 1, borderTopColor: "#eee" },
  subTitle: { fontWeight: "bold", marginBottom: 4 },
  subItem: { marginLeft: 12, marginBottom: 8 },
  subDef: { fontWeight: "600", fontSize: 12 },
  subProps: { fontFamily: "monospace", fontSize: 10, color: "#666" },
});

export default CustomAgentforceView;

Register the component and mapping in your App.tsx:

import { AppRegistry } from "react-native";
import CustomAgentforceView from "./CustomAgentforceView";
import { AgentforceService } from "@salesforce/react-native-agentforce";

// Register the component with AppRegistry -- this makes it available
// for the native bridge to render via RCTRootView / ReactRootView.
AppRegistry.registerComponent("CustomAgentforceView", () => CustomAgentforceView);

// Register the mapping with the bridge
async function setupCustomViews() {
  await AgentforceService.setViewProviderDelegate({
    componentMap: {
      "copilot/richText": "CustomAgentforceView",
      "copilot/markdown": "CustomAgentforceView",
      "copilot/recordInfo": "CustomAgentforceView",
      "copilot/list": "CustomAgentforceView",
    },
  });
}

// Call during app initialization
setupCustomViews();

Component Data Interface

interface ViewProviderComponentData {
  definition: string; // Component definition (e.g. 'copilot/richText')
  name?: string; // Component name from SDK (may be null)
  properties: Record<string, unknown>; // Key-value properties
  subComponents?: ViewProviderComponentData[]; // Nested sub-components
}

Known Component Definitions

  • copilot/list
  • copilot/recordInfo
  • copilot/markdown
  • copilot/richText

How It Works

When you register a View Provider, the following flow occurs:

  1. You register a componentMap mapping SDK definition strings to React Native component names.
  2. The native bridge stores this map in BridgeViewProvider.
  3. When the SDK needs to render a view, it calls canHandle(definition) on the view provider.
  4. If the definition matches a key in the map, the provider returns true.
  5. The SDK then calls view() (iOS) or GetView() (Android), which creates an RCTRootView (iOS) or ReactRootView (Android).
  6. Your React Native component is rendered, receiving componentData as its initial props containing the definition, name, properties, and any subComponents.

Feature Flag Requirement

The enableCustomViewProvider feature flag must be true:

await AgentforceService.configure({
  type: "service",
  // ...
  featureFlags: { enableCustomViewProvider: true },
});

Clear the Delegate

await AgentforceService.clearViewProviderDelegate();

Registration Timing

Register delegates before calling configure() to ensure they're active when the SDK initializes:

// 1. Set up delegates
AgentforceService.setLoggerDelegate({ onLog: handleLog });
AgentforceService.setNavigationDelegate({ onNavigate: handleNavigation });
await AgentforceService.setViewProviderDelegate({
  componentMap: {
    /* ... */
  },
});

// 2. Configure
await AgentforceService.configure({
  /* ... */
});

// 3. Launch
await AgentforceService.launchConversation();

Cleanup

Always clean up delegates when they're no longer needed:

// In a React component:
React.useEffect(() => {
  AgentforceService.setLoggerDelegate({ onLog: handleLog });
  AgentforceService.setNavigationDelegate({ onNavigate: handleNav });

  return () => {
    AgentforceService.clearLoggerDelegate();
    AgentforceService.clearNavigationDelegate();
  };
}, []);

Or call destroy() on app shutdown to clean up everything:

AgentforceService.destroy();

See Also