@dev-fastn-ai/react-core
Version:
React hooks and components for integrating Fastn AI connector marketplace into your applications. Built on top of @fastn-ai/core with React Query for optimal performance.
1,332 lines (1,100 loc) • 37.1 kB
Markdown
# [Fastn.ai](http://fastn.ai/) React Core Documentation
A React library for integrating **Fastn AI connectors** into your application. It provides powerful hooks to manage:
- Connector listing, activation, and deactivation
- Configuration form rendering and submission
- Seamless integration with React Query for state management
This enables you to **build fully custom UIs** on top of Fastn's powerful data and logic engine.
## 📦 Installation
Install the core library:
```bash
npm install @fastn-ai/react-core
```
Also, make sure you install the required **peer dependencies**:
```bash
npm install react react-dom @tanstack/react-query
```
> ✅ Requires React 18+
## 🏗️ Fastn Architecture Concepts
Before diving into the code, let's understand the key Fastn concepts and terminology:
### **Space (Workspace)**
A **Space** (also called Workspace) is the top-level container in Fastn that groups all your connectors, configurations, and data flows. Think of it as your project or organization's workspace where all integrations live.
### **Tenant**
A **Tenant** represents a user, team, or organization within your application. Each tenant has isolated data and configurations. For example:
- A single user account
- A team within your app
- An organization or company
- A client's workspace
### **Connector**
A **Connector** represents an integration with an external service (like Slack, Google Drive, etc.). Connectors define what external services your app can connect to.
### **Configuration**
A **Configuration** is a specific instance of a connector with saved settings and authentication. For example:
- A Slack workspace connection with specific channels selected
- A Google Drive connection with specific folders configured
- A database connection with connection parameters
### **Configuration ID**
A **Configuration ID** is a unique identifier that represents a specific configuration instance. This ID is used to:
- Load existing configurations
- Update configuration settings
- Manage the lifecycle of a specific integration
## ⚙️ Features
- **Connector Management**: List, activate, and deactivate connectors
- **Tenant Isolation**: Each tenant has its own isolated connector state and configurations
- **Configuration Persistence**: Save and retrieve configurations using unique `configurationId`s
- **Dynamic Forms**: Render configuration forms using Fastn's form schema
- **React Query Integration**: Built-in support for efficient caching and request handling
- **Authentication Flow**: Handle OAuth and custom authentication flows seamlessly
## 🚀 Getting Started
### 1. **Wrap Your App with `FastnProvider`**
This sets up Fastn in your app and gives you access to its hooks and logic.
```tsx
import { FastnProvider } from "@fastn-ai/react-core";
const fastnConfig = {
environment: "LIVE", // "LIVE", "DRAFT", or a custom environment string for widgets
authToken: "your-auth-token", // Your app's access token, authenticated through Fastn Custom Auth
tenantId: "your-tenant-id", // A unique ID representing the user, team, or organization
spaceId: "your-space-id", // Fastn Space ID (also called Workspace ID) - groups all connectors and configurations
};
function App() {
return (
<FastnProvider config={fastnConfig}>
{/* Your app components */}
</FastnProvider>
);
}
```
### 🔍 Configuration Field Reference
| Field | Description |
| ------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| `environment` | The widget environment: use `"LIVE"` for production, `"DRAFT"` for preview/testing, or any custom string configured in Fastn |
| `authToken` | The token from your authentication flow. Fastn uses this to authenticate the user via the **fastnCustomAuth** flow |
| `tenantId` | A unique identifier for the current user or organization. This helps Fastn isolate data per tenant |
| `spaceId` | The Fastn **Space ID**, also called the Workspace ID. It groups all connectors, configurations, and flows |
### 2. **Use an Existing React Query Client (Optional)**
If your app already uses React Query, you can pass your own client:
```tsx
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { FastnProvider } from "@fastn-ai/react-core";
const queryClient = new QueryClient();
const fastnConfig = {
environment: "LIVE",
authToken: "your-auth-token",
tenantId: "your-tenant-id",
spaceId: "your-space-id",
};
function App() {
return (
<QueryClientProvider client={queryClient}>
<FastnProvider config={fastnConfig}>
{/* Your app components */}
</FastnProvider>
</QueryClientProvider>
);
}
```
## 🧩 Core Hooks & Types
### **Connector Types**
```tsx
interface Connector {
id: string;
name: string;
description: string;
imageUri?: string;
status: ConnectorStatus;
actions: ConnectorAction[];
}
enum ConnectorStatus {
ACTIVE = "ACTIVE",
INACTIVE = "INACTIVE",
ALL = "ALL",
}
interface ConnectorAction {
name: string;
actionType: ConnectorActionType | string;
onClick?: () => Promise<ConnectorActionResult>;
}
interface ConnectorActionResult {
status: "SUCCESS" | "ERROR" | "CANCELLED";
}
enum ConnectorActionType {
ACTIVATION = "ACTIVATION",
DEACTIVATION = "DEACTIVATION",
NONE = "NONE",
}
```
### **Configuration Types**
```tsx
interface Configuration {
id: string;
connectorId: string;
name: string;
description: string;
imageUri?: string;
status: ConfigurationStatus;
actions: ConfigurationAction[];
}
enum ConfigurationStatus {
ENABLED = "ENABLED",
DISABLED = "DISABLED",
PENDING = "PENDING",
}
interface ConfigurationAction {
name: string;
actionType: ConfigurationActionType | string;
onClick?: () => Promise<ConfigurationActionResult>;
}
interface ConfigurationActionResult {
status: "SUCCESS" | "ERROR" | "CANCELLED";
}
enum ConfigurationActionType {
ENABLE = "ENABLE",
DISABLE = "DISABLE",
DELETE = "DELETE",
UPDATE = "UPDATE",
}
```
### **Configuration Form Types**
```tsx
type Primitive = string | number | boolean | null | undefined;
interface ConfigurationForm {
name: string;
description: string;
imageUri: string;
fields: ConnectorField[];
submitHandler: (formData: FormData) => Promise<void>;
}
type FormData =
| Record<
string,
Record<string, Primitive> | Record<string, Primitive>[] | undefined | null
>
| Record<
string,
Record<string, Primitive> | Record<string, Primitive>[] | undefined | null
>[]
| undefined
| null;
interface ConnectorField {
readonly name: string;
readonly key: string;
readonly label: string;
readonly type: ConnectorFieldType | string;
readonly required: boolean;
readonly placeholder: string;
readonly description: string;
readonly hidden?: boolean;
readonly disabled?: boolean;
readonly initialValue?:
| Record<string, Primitive>
| Record<string, Primitive>[]
| Primitive
| Primitive[];
readonly optionsSource?: SelectOptionSource;
readonly configs?: ConnectorFieldConfig;
}
interface ConnectorFieldConfig {
readonly selection?: SelectionConfig;
readonly disable?: boolean;
readonly label?: string;
readonly hideOption?: HideOptionConfig;
}
interface SelectionConfig {
readonly enable: boolean;
readonly flowId: string;
readonly sameProject: boolean;
readonly list: readonly any[];
readonly type: "OFFSET" | "CURSOR";
readonly offset: number;
readonly isIndependent: boolean;
readonly fileTypes: readonly string[];
readonly source: FlowConfig;
readonly destination: FlowConfig;
readonly isEditKeys: boolean;
readonly isAddFields: boolean;
}
interface FlowConfig {
readonly flowId: string;
readonly isSameProject: boolean;
}
interface HideOptionConfig {
readonly enable: boolean;
readonly hideBasedOnValue: boolean;
readonly key: string;
readonly operation: "!=" | "==" | ">" | "<" | ">=" | "<=" | "contains" | "not_contains";
readonly value: string;
}
```
### Configuration Usage Example
Here's an example of how the configuration types work with connector fields in React:
```tsx
// Example configuration structure
const fieldConfig: ConnectorFieldConfig = {
selection: {
enable: true,
flowId: "asanaGetWorkspaces",
sameProject: false,
list: [],
type: "OFFSET",
offset: 10,
isIndependent: false,
fileTypes: [],
source: {
flowId: "",
isSameProject: false
},
destination: {
flowId: "",
isSameProject: false
},
isEditKeys: false,
isAddFields: false
},
disable: false,
label: "Select Workspace",
hideOption: {
enable: false,
hideBasedOnValue: false,
key: "",
operation: "!=",
value: ""
}
};
// Example connector field with configuration
const connectorField: ConnectorField = {
name: "workspace",
key: "workspace",
label: "Workspace",
type: "select",
required: true,
placeholder: "Select a workspace",
description: "Choose the workspace to connect to",
configs: fieldConfig
};
// Using the field in a React component
function WorkspaceField({ field }: { field: ConnectorField }) {
const { configs } = field;
// Access configuration properties
const isSelectionEnabled = configs?.selection?.enable;
const flowId = configs?.selection?.flowId;
const isDisabled = configs?.disable;
return (
<div className="field-container">
<label>{field.label}</label>
{isSelectionEnabled && (
<p>Selection enabled for flow: {flowId}</p>
)}
<select disabled={isDisabled}>
{/* Field options */}
</select>
</div>
);
}
```
## 🔄 Complete Integration Workflows
### **Workflow 1: Setting Up Your First Slack Integration**
Let's walk through a complete example of setting up a Slack integration from scratch.
#### Step 1: List Available Connectors
First, show users what connectors are available:
```tsx
import { useConnectors } from "@fastn-ai/react-core";
function ConnectorList() {
const { data: connectors, isLoading, error } = useConnectors();
if (isLoading) return <div>Loading available integrations...</div>;
if (error) return <div>Error loading connectors: {error.message}</div>;
return (
<div className="connector-grid">
<h2>Available Integrations</h2>
{connectors?.map((connector) => (
<div key={connector.id} className="connector-card">
<img src={connector.imageUri} alt={connector.name} />
<h3>{connector.name}</h3>
<p>{connector.description}</p>
{connector.status === "ACTIVE" && (
<span className="status-badge connected">Connected</span>
)}
{connector.actions?.map((action) => (
<button
key={action.name}
onClick={action.onClick}
className={`action-btn ${action.actionType.toLowerCase()}`}
>
{action.name}
</button>
))}
</div>
))}
</div>
);
}
```
#### Step 2: List Configurations After Connector Activation
After a connector is activated, you can list its configurations:
```tsx
import { useConfigurations } from "@fastn-ai/react-core";
function ConfigurationList({ configurationId }) {
const { data: configurations, isLoading, error } = useConfigurations({ configurationId });
const [selectedConfig, setSelectedConfig] = useState(null);
if (isLoading) return <div>Loading configurations...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<div className="configuration-list">
<h2>Your Integrations</h2>
{configurations?.map((config) => (
<div key={config.id} className="config-card">
<div className="config-info">
<img src={config.imageUri} alt={config.name} />
<div>
<h3>{config.name}</h3>
<p>{config.description}</p>
</div>
</div>
<div className="config-actions">
{config.status === "ENABLED" && (
<span className="status-badge enabled">Active</span>
)}
{config.actions?.map((action) => (
<button
key={action.name}
onClick={async () => {
const result = await action.onClick();
if (action.actionType === "ENABLE" && result?.status === "SUCCESS") {
// Show configuration form for new setup
setSelectedConfig(config);
} else if (action.actionType === "UPDATE" && result?.status === "SUCCESS") {
// Show configuration form for editing
setSelectedConfig(config);
}
}}
className={`action-btn ${action.actionType.toLowerCase()}`}
>
{action.name}
</button>
))}
</div>
</div>
))}
{selectedConfig && (
<ConfigurationForm
configurationId={selectedConfig.id}
onClose={() => setSelectedConfig(null)}
/>
)}
</div>
);
}
```
#### Step 3: Load Configuration Form
When a configuration is selected (either for new setup or editing), show the configuration form:
```tsx
import { useConfigurationForm } from "@fastn-ai/react-core";
function ConfigurationForm({ configurationId, onClose }) {
const {
data: configurationForm,
isLoading,
error,
handleSubmit,
} = useConfigurationForm({ configurationId });
const [formData, setFormData] = useState({});
const [isSubmitting, setIsSubmitting] = useState(false);
// Pre-populate form with existing values if editing
useEffect(() => {
if (configurationForm?.fields) {
const initialData = {};
configurationForm.fields.forEach((field) => {
if (field.initialValue !== undefined) {
initialData[field.key] = field.initialValue;
}
});
setFormData(initialData);
}
}, [configurationForm]);
if (isLoading) return <div>Loading configuration form...</div>;
if (error) return <div>Error: {error.message}</div>;
const onSubmit = async (e) => {
e.preventDefault();
setIsSubmitting(true);
try {
await handleSubmit({ formData });
console.log("Configuration saved successfully!");
onClose();
} catch (error) {
console.error("Failed to save configuration:", error);
} finally {
setIsSubmitting(false);
}
};
return (
<div className="modal">
<form onSubmit={onSubmit} className="configuration-form">
<h2>Configure {configurationForm.name}</h2>
<p>{configurationForm.description}</p>
{configurationForm.fields.map((field) => (
<FormField
key={field.key}
field={field}
value={formData[field.key]}
onChange={(value) =>
setFormData((prev) => ({ ...prev, [field.key]: value }))
}
/>
))}
<div className="form-actions">
<button type="button" onClick={onClose}>Cancel</button>
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? "Saving..." : "Save Configuration"}
</button>
</div>
</form>
</div>
);
}
```
### **Workflow 2: Managing Existing Configurations**
Now let's show how to manage existing configurations - viewing, editing, and disabling them.
#### Step 1: List Existing Configurations
```tsx
import { useConfigurations } from "@fastn-ai/react-core";
function ConfigurationManager({ configurationId }) {
const {
data: configurations,
isLoading,
error,
} = useConfigurations({ configurationId });
const [selectedConfig, setSelectedConfig] = useState(null);
if (isLoading) return <div>Loading your integrations...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<div className="configuration-manager">
<h2>Your Integrations</h2>
{configurations?.map((config) => (
<div key={config.id} className="config-card">
<div className="config-info">
<img src={config.imageUri} alt={config.name} />
<div>
<h3>{config.name}</h3>
<p>{config.description}</p>
</div>
</div>
<div className="config-actions">
{config.status === "ENABLED" && (
<span className="status-badge enabled">Active</span>
)}
{config.actions?.map((action) => (
<button
key={action.name}
onClick={async () => {
const result = await action.onClick();
if (
action.actionType === "UPDATE" &&
result?.status === "SUCCESS"
) {
setSelectedConfig(config);
}
}}
className={`action-btn ${action.actionType.toLowerCase()}`}
>
{action.name}
</button>
))}
</div>
</div>
))}
{selectedConfig && (
<ConfigurationForm
configurationId={selectedConfig.id}
onClose={() => setSelectedConfig(null)}
/>
)}
</div>
);
}
```
#### Step 2: Disable Configuration
```tsx
function ConfigActions({ config }) {
const handleDisable = async (action) => {
if (action.actionType === "DISABLE") {
const result = await action.onClick();
if (result?.status === "SUCCESS") {
console.log("Configuration disabled successfully");
// Refresh the configuration list
}
}
};
return (
<div className="config-actions">
{config.actions?.map((action) => (
<button
key={action.name}
onClick={() => handleDisable(action)}
className={`action-btn ${action.actionType.toLowerCase()}`}
>
{action.name}
</button>
))}
</div>
);
}
```
#### Step 3: Form Field Value Handling
The form fields handle different value types based on the field type:
- **Select fields**: Always return `{ label: string, value: string }` objects
- **Multi-select fields**: Always return `{ label: string, value: string }[]` arrays
- **Google Drive picker fields**: Always return `{ label: string, value: string }` objects or arrays
- **Other fields**: Return primitive values (string, number, boolean)
```tsx
// Example form data structure
const formData = {
// Select field - single object
channel: { label: "General", value: "C123456" },
// Multi-select field - array of objects
channels: [
{ label: "General", value: "C123456" },
{ label: "Random", value: "C789012" }
],
// Google Drive picker - single object
folder: { label: "My Documents", value: "folder_id_123" },
// Google Drive picker multi - array of objects
files: [
{ label: "document1.pdf", value: "file_id_1" },
{ label: "document2.pdf", value: "file_id_2" }
],
// Text field - primitive
webhookUrl: "https://hooks.slack.com/...",
// Boolean field - primitive
enableNotifications: true
};
```
## 🎨 Form Field Components
### **Select and Multi-Select Fields**
For fields of type `select` or `multi-select`, use the `useFieldOptions` hook to handle dynamic options loading. These fields always work with `{ label, value }` objects:
```tsx
import { useFieldOptions } from "@fastn-ai/react-core";
function SelectField({ field, value, onChange, isMulti = false, context = {} }) {
// context contains all form values and is used to fetch dependent options
const {
options,
loading,
loadingMore,
hasNext,
loadMore,
error,
search,
totalLoadedOptions,
} = useFieldOptions(field, context);
function handleInputChange(e) {
search(e.target.value);
}
function handleLoadMore() {
if (hasNext && !loadingMore) loadMore();
}
function handleSelectChange(selectedOptions) {
if (isMulti) {
// For multi-select, value should be an array of { label, value } objects
const selectedValues = selectedOptions.map(option => ({
label: option.label,
value: option.value
}));
onChange(selectedValues);
} else {
// For single select, value should be a single { label, value } object
const selectedValue = selectedOptions[0] ? {
label: selectedOptions[0].label,
value: selectedOptions[0].value
} : null;
onChange(selectedValue);
}
}
return (
<div className="field-container">
<label className="field-label">
{field.label}
{field.required && <span className="required"> *</span>}
</label>
{error && (
<div className="error-message">
Error loading options: {error.message}
</div>
)}
<input
type="text"
placeholder={field.placeholder || `Search ${field.label}`}
onChange={handleInputChange}
disabled={loading}
className="search-input"
/>
<select
multiple={isMulti}
value={isMulti ? (value || []).map(v => v.value) : (value?.value || '')}
onChange={(e) => {
if (isMulti) {
const selectedOptions = Array.from(e.target.selectedOptions).map(option => {
const opt = options.find(o => o.value === option.value);
return { label: opt.label, value: opt.value };
});
handleSelectChange(selectedOptions);
} else {
const selectedOption = options.find(o => o.value === e.target.value);
handleSelectChange(selectedOption ? [selectedOption] : []);
}
}}
disabled={loading}
className="select-field"
>
{options.map((opt) => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</select>
{loading && <div className="loading">Loading options...</div>}
{hasNext && !loadingMore && (
<button
type="button"
onClick={handleLoadMore}
className="load-more-btn"
>
Load More
</button>
)}
{loadingMore && <div className="loading">Loading more...</div>}
<div className="options-info">
Loaded {totalLoadedOptions} options{hasNext ? "" : " (all loaded)"}
</div>
{field.description && (
<div className="field-description">{field.description}</div>
)}
</div>
);
}
```
### **Google Drive Picker Fields**
For Google Drive file picker fields, handle the file selection flow. These fields also work with `{ label, value }` objects and support file type filtering:
```tsx
function GoogleFilesPickerField({ field, value, onChange, isMulti = false, context = {} }) {
async function handlePickFiles() {
if (field.optionsSource?.openGoogleFilesPicker) {
await field.optionsSource.openGoogleFilesPicker({
onComplete: async (files) => {
if (isMulti) {
// For multi-select, ensure we have an array of { label, value } objects
const formattedFiles = files.map(file => ({
label: file.label || file.name || file.value,
value: file.value || file.id
}));
onChange(formattedFiles);
} else {
// For single select, ensure we have a single { label, value } object
const formattedFile = {
label: files[0]?.label || files[0]?.name || files[0]?.value,
value: files[0]?.value || files[0]?.id
};
onChange(formattedFile);
}
},
onError: async (pickerError) => {
console.error("Google Files Picker error:", pickerError);
alert("Failed to pick files: " + pickerError);
},
fileTypes: field?.optionsSource?.fileTypes,
});
}
}
return (
<div className="field-container">
<label className="field-label">
{field.label}
{field.required && <span className="required"> *</span>}
</label>
<button
type="button"
onClick={handlePickFiles}
className="google-picker-btn"
>
Pick from Google Drive
</button>
{value && (
<div className="selected-files">
<strong>Selected file{isMulti ? "s" : ""}:</strong>
<ul>
{(isMulti ? value : [value]).map((file, idx) => (
<li key={file.value || idx}>{file.label || file.value}</li>
))}
</ul>
</div>
)}
{field.description && (
<div className="field-description">{field.description}</div>
)}
</div>
);
}
```
### **Generic Form Field Component**
Create a reusable component that handles different field types with proper value handling:
```tsx
function FormField({ field, value, onChange, context = {} }) {
switch (field.type) {
case "text":
case "email":
case "password":
case "number":
return (
<div className="field-container">
<label className="field-label">
{field.label}
{field.required && <span className="required"> *</span>}
</label>
<input
type={field.type}
value={value || ""}
onChange={(e) => onChange(e.target.value)}
placeholder={field.placeholder}
disabled={field.disabled}
className="text-input"
/>
{field.description && (
<div className="field-description">{field.description}</div>
)}
</div>
);
case "checkbox":
return (
<div className="field-container">
<label className="field-label">
<input
type="checkbox"
checked={value || false}
onChange={(e) => onChange(e.target.checked)}
disabled={field.disabled}
className="checkbox-input"
/>
{field.label}
{field.required && <span className="required"> *</span>}
</label>
{field.description && (
<div className="field-description">{field.description}</div>
)}
</div>
);
case "select":
return (
<SelectField
field={field}
value={value}
onChange={onChange}
isMulti={false}
context={context}
/>
);
case "multi-select":
return (
<SelectField
field={field}
value={value}
onChange={onChange}
isMulti={true}
context={context}
/>
);
case "google-files-picker-select":
return (
<GoogleFilesPickerField
field={field}
value={value}
onChange={onChange}
isMulti={false}
context={context}
/>
);
case "google-files-picker-multi-select":
return (
<GoogleFilesPickerField
field={field}
value={value}
onChange={onChange}
isMulti={true}
context={context}
/>
);
default:
return (
<div className="field-container">
<label className="field-label">
{field.label} (Unsupported type: {field.type})
</label>
</div>
);
}
}
```
## 🔗 Field Context Passing
The React Core library supports **dynamic field dependencies** where form fields can access and respond to values from other fields in real-time. This enables complex form workflows where the options or behavior of one field depends on the selection made in another field.
### **How Context Passing Works**
When a user selects a value in one field, that value becomes available to all other fields through the `context` parameter. This allows dependent fields to:
- Load different options based on the parent field's selection
- Show/hide fields based on other field values
- Update their behavior dynamically
### **Implementation Example**
Here's how to implement context passing in your form components:
```tsx
import { useConfigurationForm } from "@fastn-ai/react-core";
function ConfigurationForm({ configurationId, connectorId, configuration }) {
const { data: configurationForm, isLoading, error } = useConfigurationForm({
configurationId,
connectorId,
configuration,
});
return (
<Formik
initialValues={initialValues}
onSubmit={handleSubmit}
>
{({ values, setFieldValue, setFieldTouched, errors, touched }) => (
<Form>
{configurationForm.fields
.filter((field) => !field.hidden)
.map((field) => (
<FormField
key={field.name}
field={field}
form={{ values, errors, touched }}
setFieldValue={setFieldValue}
setFieldTouched={setFieldTouched}
context={values} // Pass all form values as context
/>
))}
</Form>
)}
</Formik>
);
}
// FormField component that passes context to individual fields
function FormField({ field, form, setFieldValue, setFieldTouched, context }) {
const handleChange = useCallback(
(value) => {
setFieldValue(field.name, value);
setFieldTouched(field.name, true);
},
[field.name, setFieldValue, setFieldTouched]
);
switch (field.type) {
case "select":
case "multi-select":
return (
<SelectField
field={field}
onChange={handleChange}
value={form.values[field.name]}
isMulti={field.type === "multi-select"}
context={context} // Pass context to SelectField
/>
);
// ... other field types
}
}
// SelectField component that uses context
function SelectField({ field, onChange, value, isMulti, context }) {
// The useFieldOptions hook receives the context and can use it
// to fetch options based on other field values
const {
options,
loading,
loadingMore,
hasNext,
loadMore,
error,
search,
refresh,
totalLoadedOptions,
} = useFieldOptions(field, context);
const handleChange = useCallback(
(selected) => {
onChange(selected);
// When this field changes, the context is automatically updated
// and passed to all other fields
},
[onChange]
);
// ... rest of component implementation
}
```
### **Context Structure**
The `context` parameter contains all current form values in the following structure:
```tsx
// Example context object
const context = {
workspace: { label: "My Workspace", value: "ws_123" },
channel: { label: "General", value: "ch_456" },
webhookUrl: "https://hooks.slack.com/...",
enableNotifications: true,
// ... other field values
};
```
### **Use Cases**
1. **Workspace → Channel Selection**: When a user selects a workspace, the channel field loads only channels from that workspace.
2. **Database → Table Selection**: When a database is selected, the table field shows only tables from that database.
3. **Conditional Field Display**: Show/hide fields based on other field values.
4. **Dynamic Option Loading**: Load different options based on parent field selections.
### **Best Practices**
1. **Always pass context**: Make sure to pass the `context` parameter to all field components that need access to other field values.
2. **Handle loading states**: When context changes, dependent fields may need to reload their options, so handle loading states appropriately.
3. **Optimize re-renders**: Use `useCallback` and `useMemo` to prevent unnecessary re-renders when context changes.
4. **Error handling**: Handle cases where context-dependent operations fail gracefully.
### **Advanced Example: Multi-level Dependencies**
```tsx
// Example: Workspace → Channel → User selection
function MultiLevelSelectForm() {
const [formValues, setFormValues] = useState({});
return (
<div>
{/* Workspace selection */}
<SelectField
field={workspaceField}
value={formValues.workspace}
onChange={(value) => setFormValues(prev => ({ ...prev, workspace: value }))}
context={formValues}
/>
{/* Channel selection - depends on workspace */}
<SelectField
field={channelField}
value={formValues.channel}
onChange={(value) => setFormValues(prev => ({ ...prev, channel: value }))}
context={formValues} // Has access to workspace value
/>
{/* User selection - depends on both workspace and channel */}
<SelectField
field={userField}
value={formValues.user}
onChange={(value) => setFormValues(prev => ({ ...prev, user: value }))}
context={formValues} // Has access to both workspace and channel values
/>
</div>
);
}
```
### **Error Handling and Loading States**
```tsx
function ConnectorManager() {
const { data: connectors, isLoading, error, refetch } = useConnectors();
const [retryCount, setRetryCount] = useState(0);
const handleRetry = () => {
setRetryCount((prev) => prev + 1);
refetch();
};
if (isLoading) {
return (
<div className="loading-container">
<div className="spinner"></div>
<p>Loading your integrations...</p>
</div>
);
}
if (error) {
return (
<div className="error-container">
<h3>Failed to load integrations</h3>
<p>{error.message}</p>
<button onClick={handleRetry} className="retry-btn">
Retry ({retryCount} attempts)
</button>
</div>
);
}
return (
<div className="connector-list">
{connectors?.map((connector) => (
<ConnectorCard key={connector.id} connector={connector} />
))}
</div>
);
}
```
```tsx
function ConfigurationActions({ config }) {
const queryClient = useQueryClient();
const handleAction = async (action) => {
// Optimistically update the UI
queryClient.setQueryData(["configurations"], (oldData) => {
return oldData?.map((c) =>
c.id === config.id
? {
...c,
status: action.actionType === "ENABLE" ? "ENABLED" : "DISABLED",
}
: c
);
});
try {
const result = await action.onClick();
if (result?.status === "SUCCESS") {
// Invalidate and refetch to ensure consistency
queryClient.invalidateQueries(["configurations"]);
}
} catch (error) {
// Revert optimistic update on error
queryClient.invalidateQueries(["configurations"]);
console.error("Action failed:", error);
}
};
return (
<div className="config-actions">
{config.actions?.map((action) => (
<button
key={action.name}
onClick={() => handleAction(action)}
className={`action-btn ${action.actionType.toLowerCase()}`}
>
{action.name}
</button>
))}
</div>
);
}
```
## 🚨 Troubleshooting
### **Common Issues**
1. **"Invalid tenant ID" error**
- Ensure your `tenantId` is a valid string and matches your user/organization identifier
- Check that the tenant has proper permissions in your Fastn space
2. **"Space not found" error**
- Verify your `spaceId` is correct
- Ensure your auth token has access to the specified space
3. **Configuration form not loading**
- Check that the `configurationId` is valid and exists
- Ensure the connector is properly activated before trying to configure it
4. **Google Drive picker not working**
- Verify Google Drive connector is properly configured in your Fastn space
- Check that the user has granted necessary permissions
### **Debug Mode**
Enable debug logging to troubleshoot issues:
```tsx
const fastnConfig = {
environment: "LIVE",
authToken: "your-auth-token",
tenantId: "your-tenant-id",
spaceId: "your-space-id",
debug: true, // Enable debug logging
};
```
## 📚 Additional Resources
- [Fastn.ai Documentation](https://docs.fastn.ai/)
- [React Query Documentation](https://tanstack.com/query/latest)
- [Fastn Community](https://community.fastn.ai/)
## 🤝 Contributing
We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) for details.
## 📄 License
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.