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