FieldCraftDocsServicesBlogWork With Me →

Hooks

React hooks for building custom form UIs.

FieldCraft ships four React hooks for when you need finer control than FormEngineRenderer provides. All hooks use useSyncExternalStore internally for efficient reactive updates.

useFormEngine

Creates and manages a form engine instance. Returns the engine API merged with reactive state.

import { useFormEngine } from "@squaredr/fieldcraft-react";

const engine = useFormEngine(schema, {
sessionToken: "user-123",
validators: { noSpam },
});

// Engine methods
engine.setValue("name", "Alice");
engine.nextSection();
engine.jumpTo("preferences");

// Reactive state (re-renders on change)
engine.state.values; // Record<string, unknown>
engine.state.errors; // Record<string, string[]>
engine.state.currentSectionId; // string
engine.state.progressPercent; // number
engine.state.isCurrentSectionValid; // boolean

Signature

function useFormEngine(
schema: FormEngineSchema,
options?: EngineOptions,
): FormEngine & { state: FormState }

The engine is created once (via useRef) and stable across re-renders. Only the state property is reactive.

useFieldValue

Subscribe to a single field's value without re-rendering on unrelated changes.

import { useFieldValue } from "@squaredr/fieldcraft-react";

function EmailDisplay({ engine }: { engine: FormEngine }) {
const email = useFieldValue(engine, "email");
return <p>Current email: {String(email ?? "")}</p>;
}

Signature

function useFieldValue(
engine: FormEngine,
fieldId: string,
): unknown

useFieldError

Subscribe to a single field's error messages.

import { useFieldError } from "@squaredr/fieldcraft-react";

function EmailErrors({ engine }: { engine: FormEngine }) {
const errors = useFieldError(engine, "email");
// errors is string[] | undefined

if (!errors) return null;
return (
<ul>
{errors.map((msg, i) => <li key={i}>{msg}</li>)}
</ul>
);
}

Signature

function useFieldError(
engine: FormEngine,
fieldId: string,
): string[] | undefined

useSectionProgress

Subscribe to navigation state and progress information.

import { useSectionProgress } from "@squaredr/fieldcraft-react";

function ProgressHeader({ engine }: { engine: FormEngine }) {
const progress = useSectionProgress(engine);

return (
<div>
<p>
Section {progress.currentSectionIndex + 1} of{" "}
{progress.totalVisibleSections}
</p>
<div
style={{ width: `${progress.progressPercent}%` }}
className="h-1 bg-primary"
/>
<div>
<button disabled={!progress.canGoPrev} onClick={() => engine.prevSection()}>
Back
</button>
<button disabled={!progress.canGoNext} onClick={() => engine.nextSection()}>
Next
</button>
</div>
</div>
);
}

Signature

type SectionProgress = {
currentSectionId: string;
currentSectionIndex: number;
totalVisibleSections: number;
progressPercent: number;
visitedSectionIds: string[];
canGoNext: boolean;
canGoPrev: boolean;
};

function useSectionProgress(engine: FormEngine): SectionProgress

useTheme

Access the current theme from context. Must be used inside a FormEngineThemeProvider or FormEngineRenderer.

import { useTheme } from "@squaredr/fieldcraft-react";

function ThemedLabel() {
const theme = useTheme();
return (
<span style={{ fontFamily: theme.typography.fontFamily }}>
{theme.colors.primary}
</span>
);
}

Building a Custom Form UI

Combine hooks to build a completely custom form UI while keeping the engine for state management, validation, and submission:

import {
useFormEngine,
useSectionProgress,
useFieldValue,
useFieldError,
FormEngineThemeProvider,
} from "@squaredr/fieldcraft-react";

function CustomForm({ schema }: { schema: FormEngineSchema }) {
const engine = useFormEngine(schema);
const progress = useSectionProgress(engine);
const { state } = engine;

const visibleSections = engine.getVisibleSections();
const currentSection = visibleSections[progress.currentSectionIndex];
const fields = engine.getVisibleFields(currentSection.id);

return (
<FormEngineThemeProvider>
<h2>{currentSection.title}</h2>
<p>{progress.progressPercent}% complete</p>

{fields.map((field) => (
<div key={field.id}>
<label>{field.label}</label>
<input
value={String(state.values[field.id] ?? "")}
onChange={(e) => engine.setValue(field.id, e.target.value)}
onBlur={() => engine.touchField(field.id)}
/>
{state.errors[field.id]?.map((err, i) => (
<p key={i} className="text-red-500">{err}</p>
))}
</div>
))}

<button
disabled={!progress.canGoPrev}
onClick={() => engine.prevSection()}
>
Back
</button>
<button onClick={() => engine.nextSection()}>
Next
</button>
</FormEngineThemeProvider>
);
}