Skip to main content
This page documents all custom hooks used in the HubSpot Form Builder application.

useHubSpotForms

Manages HubSpot API integration for fetching forms and form schemas. Location: main/frontend/src/hooks/useHubSpotForms.ts

Return Values

forms
HubSpotForm[]
Array of available HubSpot forms
loading
boolean
Whether forms list is currently loading
error
string | null
Error message if form loading failed
connected
boolean
Whether OAuth connection to HubSpot is active
schema
FormSchema | null
Currently loaded form schema
schemaLoading
boolean
Whether schema is currently loading
schemaError
string | null
Error message if schema loading failed
fetchForms
() => Promise<void>
Fetches list of forms from HubSpot API
fetchFormSchema
(formId: string) => Promise<void>
Fetches schema for a specific form by ID
checkStatus
() => Promise<void>
Checks OAuth connection status

HubSpotForm Type

type HubSpotForm = {
  id: string;
  name: string;
  createdAt: number;
  updatedAt: number;
};

API Endpoints Used

  • GET /api/forms - List all forms
  • GET /api/forms/:formId - Get form schema
  • GET /oauth/hubspot/status - Check connection status

Error Handling

When a 401 status is received, the hook:
  • Sets connected to false
  • Throws error: “Session expired. Please reconnect to HubSpot.”
  • Updates error state with the message

Usage Example

import { useHubSpotForms } from './hooks/useHubSpotForms';

function App() {
  const {
    forms,
    loading,
    error,
    connected,
    schema,
    schemaLoading,
    schemaError,
    fetchForms,
    fetchFormSchema,
    checkStatus,
  } = useHubSpotForms();

  useEffect(() => {
    if (connected) {
      fetchForms();
    }
  }, [connected, fetchForms]);

  useEffect(() => {
    if (connected && selectedFormId) {
      fetchFormSchema(selectedFormId);
    }
  }, [connected, selectedFormId, fetchFormSchema]);

  // ... rest of component
}

useLayoutStore

Zustand store for managing form layout state and operations. Location: main/frontend/src/hooks/useLayoutStore.ts

Store Shape

layout
LayoutState | null
Current layout configuration

Actions

setLayout

layout
LayoutState
required
New layout state
setLayout: (layout: LayoutState) => void
Directly sets the entire layout state.

initializeFromSchema

schema
FormSchema
required
Form schema to initialize from
mode
'one-step' | 'multi-step'
default:"'one-step'"
Layout mode
initializeFromSchema: (schema: FormSchema, mode?: LayoutState['mode']) => void
Initializes layout from a form schema. Creates one field per row.
  • One-step mode: All fields in a single step
  • Multi-step mode: Fields split evenly between two steps

setMode

mode
'one-step' | 'multi-step'
required
New layout mode
setMode: (mode: LayoutState['mode']) => void
Switches between single-step and multi-step layouts while preserving all fields.

addStep

addStep: () => void
Adds a new empty step to the layout. Automatically:
  • Switches mode to 'multi-step'
  • Generates unique step ID
  • Names step as “Paso N” where N is step number

removeStep

stepId
string
required
ID of step to remove
removeStep: (stepId: string) => void
Removes a step and moves its fields to the first remaining step. Prevents removing the last step.

renameStep

stepId
string
required
ID of step to rename
title
string
required
New step title
renameStep: (stepId: string, title: string) => void
Updates a step’s title. Trims whitespace and defaults to “Paso” if empty.

reorderSteps

oldIndex
number
required
Current step index
newIndex
number
required
Target step index
reorderSteps: (oldIndex: number, newIndex: number) => void
Reorders steps by moving a step from one index to another.

moveFieldBetweenSteps

fieldId
string
required
Field identifier
fromStepIndex
number
required
Source step index
toStepIndex
number
required
Target step index
moveFieldBetweenSteps: (fieldId: string, fromStepIndex: number, toStepIndex: number) => void
Moves a field from one step to another. Creates a new row in the target step.

removeFieldFromStep

stepId
string
required
Step ID containing the field
fieldName
string
required
Field to remove
removeFieldFromStep: (stepId: string, fieldName: string) => void
Removes a field from a step. Empty rows are automatically cleaned up.

addFieldToNewRow

stepId
string
required
Target step ID
fieldId
string
required
Field to add
insertAtIndex
number
Row index to insert at (optional, defaults to end)
addFieldToNewRow: (stepId: string, fieldId: string, insertAtIndex?: number) => void
Adds a field from the palette to a new row in a step. Prevents duplicate fields.

addFieldToRow

stepId
string
required
Target step ID
fieldId
string
required
Field to add
rowId
string
required
Target row ID
insertAtIndex
number
required
Position within row (0-3)
addFieldToRow: (stepId: string, fieldId: string, rowId: string, insertAtIndex: number) => void
Adds a field to an existing row. Maximum 3 fields per row. Prevents duplicate fields.

moveFieldToNewRow

stepId
string
required
Step containing the field
fieldId
string
required
Field to move
insertAtIndex
number
Row index to insert at (optional, defaults to end)
moveFieldToNewRow: (stepId: string, fieldId: string, insertAtIndex?: number) => void
Moves a field to its own new row within the same step. Empty rows are cleaned up.

moveFieldWithinStep

stepId
string
required
Step containing both rows
fieldId
string
required
Field to move
fromRowId
string
required
Source row ID
toRowId
string
required
Target row ID
toFieldIndex
number
required
Position in target row
moveFieldWithinStep: (
  stepId: string,
  fieldId: string,
  fromRowId: string,
  toRowId: string,
  toFieldIndex: number
) => void
Moves a field from one row to another within the same step. Target row must have space (< 3 fields).

Helper Functions

createStepId

function createStepId(): string
Generates unique step IDs using crypto.randomUUID() or fallback timestamp-based ID.

createRowId

function createRowId(): string
Generates unique row IDs using crypto.randomUUID() or fallback timestamp-based ID.

Usage Example

import { useLayoutStore } from './hooks/useLayoutStore';

function LayoutBuilder() {
  const layout = useLayoutStore((state) => state.layout);
  const renameStep = useLayoutStore((state) => state.renameStep);
  const removeStep = useLayoutStore((state) => state.removeStep);
  const removeFieldFromStep = useLayoutStore((state) => state.removeFieldFromStep);

  // Use the actions
  const handleRenameStep = (stepId: string, title: string) => {
    renameStep(stepId, title);
  };

  // ... rest of component
}

useLayoutDnd

Manages drag-and-drop logic for the layout builder using @dnd-kit. Location: main/frontend/src/hooks/useLayoutDnd.ts

Parameters

schema
FormSchema | null
required
Current form schema

Return Values

sensors
SensorDescriptor<SensorOptions>[]
Configured dnd-kit sensors (PointerSensor with 8px activation distance)
activeId
string | null
ID of currently dragged item
activeType
'field' | 'step' | 'palette-field' | null
Type of currently dragged item
dropPositions
Map<string, 'before' | 'after' | 'inside' | null>
Visual indicators for where items will drop
handleDragStart
(event: DragStartEvent) => void
Initiates drag operation
handleDragMove
(event: DragMoveEvent) => void
Tracks mouse position during drag
handleDragOver
(event: DragOverEvent) => void
Calculates drop position and updates visual indicators
handleDragEnd
(event: DragEndEvent) => void
Executes the drop operation

Drag Types

An existing field in the layout being reordered or moved between rows/steps.
A step being reordered in multi-step mode.
A new field being dragged from the sidebar palette to the layout.

Drop Targets

field
drop target
Dropping on a field can place the dragged item before, after, or beside it (combining into same row)
step-drop
drop target
Dropping on a step’s drop zone adds the field to that step

Drop Position Logic

When dragged item is above the target field with minimal overlap.
When dragged item is below the target field with minimal overlap.
When dragged item overlaps the target field significantly. Determines left/right placement based on horizontal position.

Constraints

  • Maximum 3 fields per row
  • Fields can only move within their current step (except via step-drop zones)
  • Required fields cannot be removed from layout
  • At least one step must exist

Usage Example

import { DndContext, DragOverlay, closestCenter } from '@dnd-kit/core';
import { useLayoutDnd } from './hooks/useLayoutDnd';

function App() {
  const { schema } = useHubSpotForms();
  const dnd = useLayoutDnd(schema);

  return (
    <DndContext
      sensors={dnd.sensors}
      collisionDetection={closestCenter}
      onDragStart={dnd.handleDragStart}
      onDragMove={dnd.handleDragMove}
      onDragOver={dnd.handleDragOver}
      onDragEnd={dnd.handleDragEnd}
    >
      <LayoutBuilder schema={schema} dropPositions={dnd.dropPositions} />
      
      <DragOverlay>
        {dnd.activeId && (
          <div className="field-item-overlay">
            {/* Render dragged item preview */}
          </div>
        )}
      </DragOverlay>
    </DndContext>
  );
}

Internal Functions

findStepIndexByField

function findStepIndexByField(layout: LayoutState, fieldId: string): number
Finds the step index containing a specific field. Returns -1 if not found.

findStepIndexById

function findStepIndexById(layout: LayoutState, stepId: string): number
Finds a step’s index by its ID. Returns -1 if not found.

getPaletteFieldId

function getPaletteFieldId(rawId: string): string
Extracts field ID from palette item ID (removes palette: prefix).