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:

DelegatePurposeDirection
LoggerReceive SDK log messagesNative → JS
NavigationHandle navigation requests from the agentNative → JS
View ProviderOverride native SDK views with React Native componentsJS → 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 

1import { AgentforceService } from "@salesforce/react-native-agentforce";
2
3AgentforceService.setLoggerDelegate({
4  onLog(level, message, error) {
5    const prefix = `[Agentforce ${level.toUpperCase()}]`;
6    if (error) {
7      console.log(`${prefix} ${message} | Error: ${error}`);
8    } else {
9      console.log(`${prefix} ${message}`);
10    }
11  },
12});

Parameters 

ParameterTypeDescription
level'error' | 'warn' | 'info' | 'debug'The severity level of the log message.
messagestringThe log message from the SDK.
errorstring (optional)Stringified exception, present only for error/warn levels.

Clear the Delegate 

1AgentforceService.clearLoggerDelegate();

Platform Differences 

PlatformLog Levels Emitted
iOSerror, warn, info, debug
Androiderror, warn, info

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

Example: Analytics Forwarding 

1AgentforceService.setLoggerDelegate({
2  onLog(level, message, error) {
3    if (level === "error") {
4      analytics.track("Agentforce Error", {
5        message,
6        error: error || undefined,
7      });
8    }
9  },
10});

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 

1import { Linking } from "react-native";
2import { AgentforceService } from "@salesforce/react-native-agentforce";
3
4AgentforceService.setNavigationDelegate({
5  onNavigate(request) {
6    switch (request.type) {
7      case "link":
8        if (request.uri) {
9          Linking.openURL(request.uri as string);
10        }
11        break;
12
13      case "record":
14        // Navigate to a record detail screen in your app
15        navigation.navigate("RecordDetail", {
16          recordId: request.recordId,
17          objectType: request.objectType,
18        });
19        break;
20
21      case "quickAction":
22        console.log(`Quick action requested: ${request.actionName}`);
23        break;
24
25      case "objectHome":
26        navigation.navigate("ObjectList", {
27          objectType: request.objectType,
28        });
29        break;
30
31      default:
32        console.log("Unhandled navigation:", JSON.stringify(request));
33    }
34  },
35});

Navigation Request Types 

TypeDescriptionFields
recordNavigate to a Salesforce recordrecordId, objectType, pageReference?
linkOpen an external or internal URLuri, pageReference?
quickActionExecute a Salesforce quick actionactionName, recordId?, objectType?
pageReferenceNavigate to a Lightning page referencepageReference
objectHomeNavigate to an object’s home/list pageobjectType, pageReference?
appNavigate to an app or external packagepackageName, uri?
unknownCatch-all for unrecognized typesraw

Clear the Delegate 

1AgentforceService.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

1import { AppRegistry } from "react-native";
2import { ViewProviderComponentData } from "@salesforce/react-native-agentforce";
3
4function CustomRichTextView({ componentData }: { componentData: ViewProviderComponentData }) {
5  const text = componentData.properties.text as string;
6  return (
7    <View style={{ padding: 12 }}>
8      <Text style={{ fontSize: 16 }}>{text}</Text>
9    </View>
10  );
11}
12
13// Register it with AppRegistry
14AppRegistry.registerComponent("CustomRichTextView", () => CustomRichTextView);

Part 2: Register the component map with the bridge

1await AgentforceService.setViewProviderDelegate({
2  componentMap: {
3    "copilot/richText": "CustomRichTextView",
4    "copilot/markdown": "CustomMarkdownView",
5  },
6});

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:

1// CustomAgentforceView.tsx
2import React from "react";
3import { View, Text, ScrollView, StyleSheet } from "react-native";
4import type { ViewProviderComponentData } from "@salesforce/react-native-agentforce";
5
6interface Props {
7  componentData: ViewProviderComponentData;
8}
9
10function CustomAgentforceView({ componentData }: Props) {
11  const { definition, name, properties, subComponents } = componentData;
12
13  return (
14    <View style={styles.container}>
15      <View style={styles.header}>
16        <Text style={styles.defLabel}>{definition}</Text>
17        {name && <Text style={styles.nameLabel}>{name}</Text>}
18      </View>
19
20      <ScrollView style={styles.propsContainer}>
21        {Object.entries(properties).map(([key, value]) => (
22          <View key={key} style={styles.propRow}>
23            <Text style={styles.propKey}>{key}:</Text>
24            <Text style={styles.propValue}>
25              {typeof value === "object" ? JSON.stringify(value) : String(value)}
26            </Text>
27          </View>
28        ))}
29      </ScrollView>
30
31      {subComponents && subComponents.length > 0 && (
32        <View style={styles.subSection}>
33          <Text style={styles.subTitle}>Sub-Components ({subComponents.length})</Text>
34          {subComponents.map((sub, index) => (
35            <View key={index} style={styles.subItem}>
36              <Text style={styles.subDef}>{sub.definition}</Text>
37              <Text style={styles.subProps}>{JSON.stringify(sub.properties, null, 2)}</Text>
38            </View>
39          ))}
40        </View>
41      )}
42    </View>
43  );
44}
45
46const styles = StyleSheet.create({
47  container: {
48    margin: 8,
49    padding: 12,
50    backgroundColor: "#f8f9fa",
51    borderRadius: 8,
52    borderWidth: 1,
53    borderColor: "#dee2e6",
54  },
55  header: { marginBottom: 8 },
56  defLabel: { fontSize: 14, fontWeight: "bold", color: "#0176D3" },
57  nameLabel: { fontSize: 12, color: "#666", marginTop: 2 },
58  propsContainer: { maxHeight: 200 },
59  propRow: { flexDirection: "row", marginBottom: 4 },
60  propKey: { fontWeight: "600", marginRight: 8, color: "#333" },
61  propValue: { flex: 1, color: "#555" },
62  subSection: { marginTop: 12, paddingTop: 8, borderTopWidth: 1, borderTopColor: "#eee" },
63  subTitle: { fontWeight: "bold", marginBottom: 4 },
64  subItem: { marginLeft: 12, marginBottom: 8 },
65  subDef: { fontWeight: "600", fontSize: 12 },
66  subProps: { fontFamily: "monospace", fontSize: 10, color: "#666" },
67});
68
69export default CustomAgentforceView;

Register the component and mapping in your App.tsx:

1import { AppRegistry } from "react-native";
2import CustomAgentforceView from "./CustomAgentforceView";
3import { AgentforceService } from "@salesforce/react-native-agentforce";
4
5// Register the component with AppRegistry -- this makes it available
6// for the native bridge to render via RCTRootView / ReactRootView.
7AppRegistry.registerComponent("CustomAgentforceView", () => CustomAgentforceView);
8
9// Register the mapping with the bridge
10async function setupCustomViews() {
11  await AgentforceService.setViewProviderDelegate({
12    componentMap: {
13      "copilot/richText": "CustomAgentforceView",
14      "copilot/markdown": "CustomAgentforceView",
15      "copilot/recordInfo": "CustomAgentforceView",
16      "copilot/list": "CustomAgentforceView",
17    },
18  });
19}
20
21// Call during app initialization
22setupCustomViews();

Component Data Interface 

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

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:

1await AgentforceService.configure({
2  type: "service",
3  // ...
4  featureFlags: { enableCustomViewProvider: true },
5});

Clear the Delegate 

1await AgentforceService.clearViewProviderDelegate();

Registration Timing 

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

1// 1. Set up delegates
2AgentforceService.setLoggerDelegate({ onLog: handleLog });
3AgentforceService.setNavigationDelegate({ onNavigate: handleNavigation });
4await AgentforceService.setViewProviderDelegate({
5  componentMap: {
6    /* ... */
7  },
8});
9
10// 2. Configure
11await AgentforceService.configure({
12  /* ... */
13});
14
15// 3. Launch
16await AgentforceService.launchConversation();

Cleanup 

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

1// In a React component:
2React.useEffect(() => {
3  AgentforceService.setLoggerDelegate({ onLog: handleLog });
4  AgentforceService.setNavigationDelegate({ onNavigate: handleNav });
5
6  return () => {
7    AgentforceService.clearLoggerDelegate();
8    AgentforceService.clearNavigationDelegate();
9  };
10}, []);

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

1AgentforceService.destroy();

See Also