npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

@fastn-ai/react-core

v1.0.12

Published

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.

Downloads

54

Readme

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:

npm install @fastn-ai/react-core

Also, make sure you install the required peer dependencies:

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 configurationIds
  • 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.

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:

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

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

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

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:

// 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:

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:

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:

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

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

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)
// 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:

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:

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:

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:

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:

// 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

// 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

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>
  );
}
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:

const fastnConfig = {
  environment: "LIVE",
  authToken: "your-auth-token",
  tenantId: "your-tenant-id",
  spaceId: "your-space-id",
  debug: true, // Enable debug logging
};

📚 Additional Resources


🤝 Contributing

We welcome contributions! Please see our Contributing Guide for details.


📄 License

This project is licensed under the MIT License - see the LICENSE file for details.