This page documents all custom hooks used in the HubSpot Form Builder application.
Manages HubSpot API integration for fetching forms and form schemas.
Location: main/frontend/src/hooks/useHubSpotForms.ts
Return Values
Array of available HubSpot forms
Whether forms list is currently loading
Error message if form loading failed
Whether OAuth connection to HubSpot is active
Currently loaded form schema
Whether schema is currently loading
Error message if schema loading failed
Fetches list of forms from HubSpot API
fetchFormSchema
(formId: string) => Promise<void>
Fetches schema for a specific form by ID
Checks OAuth connection status
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
Current layout configuration
Actions
setLayout
setLayout: (layout: LayoutState) => void
Directly sets the entire layout state.
initializeFromSchema
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
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
removeStep: (stepId: string) => void
Removes a step and moves its fields to the first remaining step. Prevents removing the last step.
renameStep
renameStep: (stepId: string, title: string) => void
Updates a step’s title. Trims whitespace and defaults to “Paso” if empty.
reorderSteps
reorderSteps: (oldIndex: number, newIndex: number) => void
Reorders steps by moving a step from one index to another.
moveFieldBetweenSteps
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
Step ID containing the field
removeFieldFromStep: (stepId: string, fieldName: string) => void
Removes a field from a step. Empty rows are automatically cleaned up.
addFieldToNewRow
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
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
Step containing the field
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
Step containing both rows
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)
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
Dropping on a field can place the dragged item before, after, or beside it (combining into same row)
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).