| title | Hooks |
|---|---|
| order | 1 |
Hooks give you full control over form rendering while the SDK manages data fetching, validation, submission, and error handling. Each hook returns pre-bound field components, metadata, and actions — you supply the layout and labels.
Hooks are an experimental feature. APIs may change between minor versions during 0.x.x releases.
| Hook | Description | Reference |
|---|---|---|
useEmployeeDetailsForm |
Create or update employee profile fields (name, email, SSN, date of birth, self-onboarding) | useEmployeeDetailsForm |
useCompensationForm |
Create or update job compensation (job title, FLSA status, pay rate, payment unit, minimum wage adjustments) | useCompensationForm |
useWorkAddressForm |
Create or update an employee's work address (company location select, effective date) | useWorkAddressForm |
usePayScheduleForm |
Create or update a company pay schedule (frequency, pay dates, pay period calendar preview) | usePayScheduleForm |
useSignCompanyForm |
Sign a company form (PDF viewer, typed signature, confirmation checkbox) | useSignCompanyForm |
useSignEmployeeForm |
Sign an employee form (signature, confirmation, I-9 preparer/translator sections) | useSignEmployeeForm |
All hooks are exported from @gusto/embedded-react-sdk. Your app must be wrapped in GustoProvider.
import { GustoProvider, useEmployeeDetailsForm } from '@gusto/embedded-react-sdk'
function App() {
return (
<GustoProvider config={{ apiToken: '...' }}>
<EmployeeForm companyId="company-uuid" />
</GustoProvider>
)
}
function EmployeeForm({ companyId }: { companyId: string }) {
const employeeDetails = useEmployeeDetailsForm({ companyId })
if (employeeDetails.isLoading) {
return <div>Loading...</div>
}
const { Fields } = employeeDetails.form
return (
<form
onSubmit={async e => {
e.preventDefault()
await employeeDetails.actions.onSubmit()
}}
>
<Fields.FirstName label="First name" formHookResult={employeeDetails} />
<Fields.LastName label="Last name" formHookResult={employeeDetails} />
<button type="submit">Save</button>
</form>
)
}- Call the hook with the required identifiers (
companyId,employeeId, etc.) - Check
isLoading— the hook fetches server data before the form is ready - Connect fields — pass
formHookResultas a prop to each field, or wrap fields inSDKFormProviderfor context-based connection (see Connecting Fields to the Form) - Render
Fields— each field is a pre-bound component that handles validation, error display, and metadata automatically - Call
onSubmit— the hook handles API mutations, error normalization, and returns the saved entity
Every Field component needs access to form state — react-hook-form's control, field metadata (required/disabled), and error messages. There are two ways to provide this connection:
Pass the hook result directly to each field. No wrapper component needed.
const { Fields } = employeeDetails.form
<Fields.FirstName label="First name" formHookResult={employeeDetails} />
<Fields.LastName label="Last name" formHookResult={employeeDetails} />
<Fields.Email label="Email" formHookResult={employeeDetails} />Each field reads metadata, form control, and error state directly from the prop. This is the most flexible approach — fields can be placed anywhere in your component tree and interleaved freely with fields from other hooks.
Wrap fields in SDKFormProvider and they pick up form state from context automatically.
import { SDKFormProvider } from '@gusto/embedded-react-sdk'
;<SDKFormProvider formHookResult={employeeDetails}>
<Fields.FirstName label="First name" />
<Fields.LastName label="Last name" />
<Fields.Email label="Email" />
</SDKFormProvider>Fields inside an SDKFormProvider don't need the formHookResult prop — the provider injects form state via React context. This is convenient when all fields from a single hook are grouped together.
Both approaches produce identical validation, API payloads, and behavior. The difference is purely in how fields discover their form state.
formHookResult prop |
SDKFormProvider |
|
|---|---|---|
| Best for | Interleaving fields from multiple hooks; maximum layout flexibility | Grouping fields from a single hook together |
| Boilerplate | Each field receives the prop | One wrapper, fields are clean |
| Interleaving | Fields from different hooks can be placed in any order | Fields must stay within their provider boundary |
| API error syncing | Handled automatically per-field | Handled automatically via the provider |
You can use different approaches for different hooks on the same page — for example, SDKFormProvider for one hook's fields that are grouped together, and formHookResult props for another hook's fields that are scattered. See Composing Multiple Hooks for examples.
Avoid passing
formHookResultto fields via props that are already inside anSDKFormProvider. When both are present on the same field, the prop takes precedence and the provider's context is ignored, which may lead to unexpected behavior.
By default, every field component renders through the SDK's Component Adapter. If you've configured a Component Adapter for your app (e.g., mapping to your own design system), hook fields will automatically render using your custom components. If no adapter is configured, fields render using the SDK's built-in React Aria-driven components.
This means hooks inherit whatever UI customization you've already set up at the GustoProvider level -- no extra configuration needed.
If you need a specific field to render differently without changing your global Component Adapter, most fields accept a FieldComponent prop. This lets you swap the UI for a single field by providing your own component that conforms to the expected props interface.
The FieldComponent receives the same props the underlying UI primitive expects (TextInputProps, SelectProps, NumberInputProps, etc.) -- including value, onChange, onBlur, error state, and accessibility attributes. You don't need any react-hook-form knowledge; the hook field handles all form binding and passes clean UI props to your component.
import type { TextInputProps } from '@gusto/embedded-react-sdk'
function MyCustomTextInput(props: TextInputProps) {
return (
<div className="my-field-wrapper">
<label>{props.label}</label>
<input
value={props.value}
onChange={e => props.onChange?.(e.target.value)}
onBlur={props.onBlur}
disabled={props.isDisabled}
required={props.isRequired}
/>
{props.errorMessage && <span className="error">{props.errorMessage}</span>}
</div>
)
}
;<Fields.FirstName
label="First name"
FieldComponent={MyCustomTextInput}
validationMessages={{ REQUIRED: 'First name is required' }}
/>This is useful when you want to use a third-party input library for one field, add custom styling, or render a completely different control while still getting the hook's validation, error handling, and form binding for free.
The FieldComponent prop is available on all field types: TextInputProps, SelectProps, NumberInputProps, CheckboxProps, DatePickerProps, RadioGroupProps, and SwitchProps. Import the corresponding prop type from @gusto/embedded-react-sdk for type-safe implementations.
Every form hook returns a data object when ready. This contains the entities fetched by the hook — the primary entity being edited plus any supporting data needed for the form.
if (!employeeDetails.isLoading) {
const { employee } = employeeDetails.data
// employee is the loaded Employee entity (or null in create mode)
}The shape of data varies by hook — see each hook's reference page for details:
useEmployeeDetailsForm—{ employee }useCompensationForm—{ compensation, jobs, currentJob, minimumWages }useWorkAddressForm—{ workAddress, workAddresses, companyLocations }usePayScheduleForm—{ paySchedule, payPeriodPreview, payPreviewLoading, paymentSpeedDays }useSignCompanyForm—{ companyForm, pdfUrl }
Hooks let you declare which form fields are required beyond the built-in defaults. Each hook has built-in requiredness rules based on the form mode (create vs. update), and you can override optional fields to be required.
The API varies by hook. Some hooks use requiredFields (flat array or per-mode object), while newer hooks use optionalFieldsToRequire with type-safe, mode-aware overrides derived from the schema configuration.
Pass a flat array (applies to both modes) or an object with per-mode arrays:
// Flat array: same requirements for both create and update
useEmployeeDetailsForm({
companyId,
requiredFields: ['email', 'dateOfBirth'],
})
// Per-mode object: different requirements per mode
useEmployeeDetailsForm({
companyId,
requiredFields: {
create: ['email'],
update: ['ssn', 'dateOfBirth'],
},
})Override specific fields that are optional in a given mode to be required. The type constrains which fields can be listed per mode — only fields that are actually optional in that mode are allowed:
useCompensationForm({
employeeId,
optionalFieldsToRequire: {
update: ['jobTitle', 'rate'],
},
})Each hook's reference page documents which fields are available to require and which are required by default in each mode. See:
- useEmployeeDetailsForm required fields
- useCompensationForm configurable required fields
- useWorkAddressForm required fields
- usePayScheduleForm configurable required fields
All form hooks accept a defaultValues prop to pre-fill the form. Pass a partial object matching the hook's form data shape — any fields you omit use built-in fallbacks (typically empty strings or false).
useEmployeeDetailsForm({
companyId,
defaultValues: {
firstName: 'Jane',
email: '[email protected]',
},
})
useCompensationForm({
defaultValues: {
jobTitle: 'Software Engineer',
rate: 85000,
paymentUnit: 'Year',
},
})In create mode (no existing entity), defaultValues populate the form directly. In update mode, server data always takes precedence — defaultValues only fill in fields the server doesn't provide.
Each hook's reference page documents the full form data shape accepted by defaultValues:
- useEmployeeDetailsForm form data
- useCompensationForm form data
- useWorkAddressForm form data
- usePayScheduleForm form data
- useSignCompanyForm form data
Every hook returns a discriminated union on isLoading. While server data is being fetched, only isLoading and errorHandling are available:
const employeeDetails = useEmployeeDetailsForm({ companyId, employeeId })
// Loading branch — no form data yet
if (employeeDetails.isLoading) {
return <LoadingSpinner />
}
// Ready branch — TypeScript narrows to the full return type
const { data, form, actions, status, errorHandling } = employeeDetailsThe loading state is also where you first encounter errors — if a data-fetching query fails, the hook stays in the loading branch but errorHandling.errors will be populated. See Error Handling below.
All hooks return an errorHandling object in both loading and ready states. This ensures you can always display errors and offer recovery actions, even when data never loaded.
interface HookErrorHandling {
errors: SDKError[]
retryQueries: () => void
clearSubmitError: () => void
}When a screen pulls from more than one SDK hook (or mixes SDK hooks with additional @gusto/embedded-api queries), combine their error state into one banner and one retry/dismiss flow using composeErrorHandler / composeSubmitHandler. See Composing Multiple Hooks.
interface SDKError {
category: 'api_error' | 'validation_error' | 'network_error' | 'internal_error'
message: string
httpStatus?: number
fieldErrors: SDKFieldError[]
raw?: unknown
}
interface SDKFieldError {
field: string
category: string
message: string
metadata?: Record<string, unknown>
}| Category | What happened | What you should do |
|---|---|---|
api_error |
HTTP error from the Gusto API (422, 404, 409, etc.) | Display error.message. For 422 responses, check error.fieldErrors for inline field-level messages. For 404/409, show a contextual message to the user. |
validation_error |
Client-side schema validation failed before the request was sent | This is likely an SDK bug. Display a generic error and report to Gusto. |
network_error |
Network connectivity failure (timeout, connection refused) | Show retry UI using errorHandling.retryQueries(). Suggest the user check their connection. |
internal_error |
Unexpected SDK runtime error | Display a generic error and report to Gusto. |
retryQueries()— Retries all failed data-fetching queries. Dependent queries automatically re-trigger when their dependencies resolve.clearSubmitError()— Clears the most recent form submission error from state.
Explicit query vs submit labels on each SDKError are not part of the type today; infer recovery from retryQueries (fetch) vs clearSubmitError (submit). A future revision may add structured discrimination.
function EmployeeForm({ companyId }: { companyId: string }) {
const employeeDetails = useEmployeeDetailsForm({ companyId })
if (employeeDetails.isLoading) {
const { errors, retryQueries } = employeeDetails.errorHandling
if (errors.length > 0) {
return (
<div>
<p>Failed to load employee data.</p>
<ul>
{errors.map((error, i) => (
<li key={i}>{error.message}</li>
))}
</ul>
<button onClick={retryQueries}>Retry</button>
</div>
)
}
return <LoadingSpinner />
}
// ... render form
}Submit errors (from API mutations) are also collected into errorHandling.errors. After a failed submission, you can display the error and let the user correct their input:
const { errors, clearSubmitError } = employeeDetails.errorHandling
{
errors.length > 0 && (
<div role="alert">
{errors.map((error, i) => (
<p key={i}>{error.message}</p>
))}
</div>
)
}Field-level API errors (e.g., 422 responses with fieldErrors) are automatically synced to the corresponding form fields so they appear inline alongside client-side validation errors. When using SDKFormProvider, the provider handles this syncing via context. When using the formHookResult prop, each field resolves errors directly from formHookResult.errorHandling.errors — no provider is needed.
For a deeper look at the SDK's error architecture, see Error Handling in the React SDK and Observability.
Each hook's actions.onSubmit is an async function that validates the form, calls the appropriate API mutations, and returns the result.
interface HookSubmitResult<T> {
mode: 'create' | 'update'
data: T
}onSubmit accepts optional callbacks that fire after each mutation step. This is useful for telemetry logging or reacting to individual API call results:
const result = await employeeDetails.actions.onSubmit({
onEmployeeCreated: employee => {
console.log('Created:', employee.uuid)
},
onEmployeeUpdated: employee => {
console.log('Updated:', employee.uuid)
},
})
if (result) {
// result.mode is 'create' or 'update'
// result.data is the saved Employee entity
navigate(`/employees/${result.data.uuid}`)
}If validation fails, onSubmit returns undefined and the form fields display their error messages. If a mutation fails, the error is captured in errorHandling.errors.
Use status.isPending to disable the submit button while mutations are in flight, and status.mode to adapt your UI based on whether the hook is creating or updating:
<h2>{employeeDetails.status.mode === 'create' ? 'Add Employee' : 'Edit Employee'}</h2>
<button type="submit" disabled={employeeDetails.status.isPending}>
{employeeDetails.status.isPending ? 'Saving...' : 'Save'}
</button>status.mode is 'create' when no existing entity was loaded (e.g., no employeeId was provided) and 'update' when editing an existing record.
Each field component accepts a validationMessages prop that maps error codes to human-readable strings. Error codes are defined as typed constants, and TypeScript enforces that you provide a message for every code the field can produce.
import { EmployeeDetailsErrorCodes } from '@gusto/embedded-react-sdk'
<Fields.FirstName
label="First name"
validationMessages={{
REQUIRED: 'First name is required',
INVALID_NAME: 'Enter a valid first name',
}}
/>
<Fields.Email
label="Email"
validationMessages={{
REQUIRED: 'Email is required',
INVALID_EMAIL: 'Please enter a valid email address',
EMAIL_REQUIRED_FOR_SELF_ONBOARDING: 'Email is required when self-onboarding is enabled',
}}
/>If you omit validationMessages, validation still runs but no message is displayed — the field is marked as invalid without explanatory text.
Error codes for each hook are exported alongside the hook:
EmployeeDetailsErrorCodes— see useEmployeeDetailsForm field referenceCompensationErrorCodes— see useCompensationForm field referenceWorkAddressErrorCodes— see useWorkAddressForm field referencePayScheduleErrorCodes— see usePayScheduleForm field referenceSignCompanyFormErrorCodes— see useSignCompanyForm field reference
A screen that combines multiple SDK hooks, or mixes SDK hooks with additional @gusto/embedded-api queries, produces multiple errorHandling objects and (for form screens) multiple submit flows. Two small helpers stitch them together:
composeErrorHandler([sources])— merges many error sources into a singleHookErrorHandling.composeSubmitHandler([forms], onAllValid)— coordinates validation and ordered submits across forms, and returns{ handleSubmit, errorHandling }whereerrorHandlingis built from those forms viacomposeErrorHandlerunder the hood.
Use composeErrorHandler to produce a single errorHandling bag for any screen that reads from multiple sources. It accepts any mix of:
- SDK hook results — objects with an
errorHandlingproperty (e.g.,useEmployeeDetailsForm,useCompensationForm, or the return value ofcomposeSubmitHandler). @gusto/embedded-apiReact Query results — objects witherrorandrefetchproperties.
import { composeErrorHandler, useEmployeeDetailsForm } from '@gusto/embedded-react-sdk'
import { useEmployeeFormsList } from '@gusto/embedded-api/react-query/employeeFormsList'
function EmployeeProfileView({ companyId, employeeId }: { companyId: string; employeeId: string }) {
const employeeDetails = useEmployeeDetailsForm({ companyId, employeeId })
const formsListQuery = useEmployeeFormsList({ employeeId })
const errorHandling = composeErrorHandler([employeeDetails, formsListQuery])
if (errorHandling.errors.length > 0) {
return (
<div role="alert">
{errorHandling.errors.map((error, i) => (
<p key={i}>{error.message}</p>
))}
<button onClick={errorHandling.retryQueries}>Retry</button>
</div>
)
}
// ...render
}employeeDetails is an SDK hook result (its errorHandling is delegated into), while formsListQuery is a raw @gusto/embedded-api query (its error is normalized and its refetch is wired into retryQueries). The same call works for any combination of the two shapes.
The returned errorHandling has the same shape as any SDK hook's errorHandling:
errors: SDKError[]— fetch errors from all sources.retryQueries()— refetches every failed query and delegates into nested hooks so their retries fire too.clearSubmitError()— clears submit errors across any nested hook results passed in.
When multiple forms sit on the same page (e.g., employee details and compensation side by side), use composeSubmitHandler to coordinate validation, focus, and ordered submission across all of them. It returns both pieces you typically need:
handleSubmit— a form event handler that validates every form in parallel, focuses the first invalid field across forms (in array order) if any fail, and calls youronAllValidcallback only when every form passes.errorHandling— a combinedHookErrorHandlingbuilt from the forms viacomposeErrorHandlerinternally. No need to callcomposeErrorHandleryourself for the common case.
const { handleSubmit, errorHandling } = composeSubmitHandler(
[employeeDetails, compensation],
async () => {
await employeeDetails.actions.onSubmit()
await compensation.actions.onSubmit()
},
)If the same screen also has extra @gusto/embedded-api queries that should feed the same error banner, pass the composeSubmitHandler result back into composeErrorHandler alongside those queries — the result already satisfies composeErrorHandler's input shape:
const submitResult = composeSubmitHandler([employeeDetails, compensation], onAllValid)
const errorHandling = composeErrorHandler([submitResult, extraQuery])Each form hook must be initialized with shouldFocusError: false so that react-hook-form's per-form focus is disabled and composeSubmitHandler can manage cross-form focus instead.
Both connection approaches work with composition. Choose the one that fits your layout.
When fields from each hook are grouped into their own sections, SDKFormProvider keeps things clean:
import {
useEmployeeDetailsForm,
useCompensationForm,
composeSubmitHandler,
SDKFormProvider,
} from '@gusto/embedded-react-sdk'
function OnboardingPage({ companyId, employeeId }: { companyId: string; employeeId: string }) {
const employeeDetails = useEmployeeDetailsForm({
companyId,
employeeId,
shouldFocusError: false,
})
const compensation = useCompensationForm({
employeeId,
shouldFocusError: false,
})
if (employeeDetails.isLoading || compensation.isLoading) {
return <LoadingSpinner />
}
const EmployeeDetailsFields = employeeDetails.form.Fields
const CompensationFields = compensation.form.Fields
const { handleSubmit, errorHandling } = composeSubmitHandler(
[employeeDetails, compensation],
async () => {
await employeeDetails.actions.onSubmit()
await compensation.actions.onSubmit()
},
)
return (
<form onSubmit={handleSubmit}>
{errorHandling.errors.length > 0 && (
<div role="alert">
{errorHandling.errors.map((error, i) => (
<p key={i}>{error.message}</p>
))}
</div>
)}
<SDKFormProvider formHookResult={employeeDetails}>
<h2>Employee Details</h2>
<EmployeeDetailsFields.FirstName label="First name" />
<EmployeeDetailsFields.LastName label="Last name" />
</SDKFormProvider>
<SDKFormProvider formHookResult={compensation}>
<h2>Compensation</h2>
<CompensationFields.JobTitle label="Job title" />
<CompensationFields.Rate label="Pay rate" />
</SDKFormProvider>
<button type="submit">Save All</button>
</form>
)
}Each SDKFormProvider scopes field metadata and error syncing to its respective hook. The outer <form> element uses the composed submit handler, and the combined errorHandling drives a single banner covering fetch failures from either hook and submit failures from any of the onSubmit calls.
When you want to mix fields from different hooks in any order — for example, placing job title next to first name, or grouping fields by theme rather than domain — use the formHookResult prop. There are no provider boundaries to manage, so fields can go anywhere:
import {
useEmployeeDetailsForm,
useCompensationForm,
useWorkAddressForm,
composeSubmitHandler,
} from '@gusto/embedded-react-sdk'
function OnboardingPage({ companyId, employeeId }: { companyId: string; employeeId: string }) {
const employeeDetails = useEmployeeDetailsForm({
companyId,
employeeId,
shouldFocusError: false,
})
const compensation = useCompensationForm({
employeeId,
shouldFocusError: false,
})
const workAddress = useWorkAddressForm({
companyId,
employeeId,
shouldFocusError: false,
})
if (employeeDetails.isLoading || compensation.isLoading || workAddress.isLoading) {
return <LoadingSpinner />
}
const EmployeeDetailsFields = employeeDetails.form.Fields
const CompensationFields = compensation.form.Fields
const WorkAddressFields = workAddress.form.Fields
const { handleSubmit, errorHandling } = composeSubmitHandler(
[employeeDetails, compensation, workAddress],
async () => {
await employeeDetails.actions.onSubmit()
await compensation.actions.onSubmit()
await workAddress.actions.onSubmit()
},
)
return (
<form onSubmit={handleSubmit}>
{errorHandling.errors.length > 0 && (
<div role="alert">
{errorHandling.errors.map((error, i) => (
<p key={i}>{error.message}</p>
))}
</div>
)}
<section>
<h2>Who</h2>
<EmployeeDetailsFields.FirstName label="First name" formHookResult={employeeDetails} />
<EmployeeDetailsFields.LastName label="Last name" formHookResult={employeeDetails} />
<EmployeeDetailsFields.Email label="Email" formHookResult={employeeDetails} />
<CompensationFields.StartDate label="Start date" formHookResult={compensation} />
</section>
<section>
<h2>Role and Location</h2>
<CompensationFields.JobTitle label="Job title" formHookResult={compensation} />
<WorkAddressFields.Location label="Work address" formHookResult={workAddress} />
<CompensationFields.Rate label="Pay rate" formHookResult={compensation} />
<CompensationFields.PaymentUnit label="Pay frequency" formHookResult={compensation} />
</section>
<button type="submit">Save All</button>
</form>
)
}Fields from employeeDetails, compensation, and workAddress are freely interleaved — each field knows which hook it belongs to via its formHookResult prop. Validation, error handling, and submission all work identically to the provider-based approach.
In a create flow, the employee doesn't exist yet — so useCompensationForm and useWorkAddressForm can't receive an employeeId at init time. Both hooks accept employeeId as optional in their props and allow it to be provided at submit time via the options parameter:
function CreateOnboardingPage({ companyId }: { companyId: string }) {
const employeeDetails = useEmployeeDetailsForm({
companyId,
shouldFocusError: false,
})
const compensation = useCompensationForm({
shouldFocusError: false,
})
const workAddress = useWorkAddressForm({
companyId,
shouldFocusError: false,
})
// ...loading checks...
const { handleSubmit } = composeSubmitHandler(
[employeeDetails, compensation, workAddress],
async () => {
const employeeResult = await employeeDetails.actions.onSubmit()
if (!employeeResult) return
const newEmployeeId = employeeResult.data.uuid
await compensation.actions.onSubmit(undefined, { employeeId: newEmployeeId })
await workAddress.actions.onSubmit(undefined, { employeeId: newEmployeeId })
},
)
// ...render forms...
}When employeeId is omitted from props, the hooks skip data fetching and render in create mode with empty defaults. The ID is resolved at submit time, avoiding re-render cycles that would tear down the form UI.
composeSubmitHandler takes care of client-side validation — your onAllValid callback only runs when every form passes. However, API mutations inside the callback can still fail. When they do, onSubmit returns undefined (it never throws) and the error is automatically captured in errorHandling.errors for display.
Early return when a subsequent call depends on data from a prior call:
const { handleSubmit, errorHandling } = composeSubmitHandler(
[employeeDetails, compensation, workAddress],
async () => {
const employeeResult = await employeeDetails.actions.onSubmit()
if (!employeeResult) return
const newEmployeeId = employeeResult.data.uuid
await compensation.actions.onSubmit(undefined, { employeeId: newEmployeeId })
await workAddress.actions.onSubmit(undefined, { employeeId: newEmployeeId })
},
)Here compensation and workAddress both need the employee ID, so if employee creation fails there's nothing to pass and no reason to continue. The user will see the error from errorHandling.errors and can retry.
For independent submissions where one doesn't depend on the other's result, continuing after a failure is a valid choice — it depends on your product requirements.
Each hook exposes form.getFormSubmissionValues() — a synchronous function that returns the current form values parsed through the hook's Zod validation schema. The returned object matches exactly what onSubmit would receive: all preprocessing transforms (e.g., string-to-number coercion) are applied.
Returns undefined when the current form state is invalid (empty required fields, failed cross-field rules, etc.). It never throws.
const values = employeeDetails.form.getFormSubmissionValues()
if (values) {
console.log(values.firstName, values.lastName)
}This is particularly useful when you need to share values across form submissions. For example, when the work address form captures an effective date that the compensation form needs as its start date, you can read the value from one form and pass it to the other's submit options:
const workAddress = useWorkAddressForm({ companyId, shouldFocusError: false })
const compensation = useCompensationForm({
withStartDateField: false,
shouldFocusError: false,
})
// ...loading checks...
const { handleSubmit } = composeSubmitHandler(
[employeeDetails, workAddress, compensation],
async () => {
const employeeResult = await employeeDetails.actions.onSubmit()
if (!employeeResult) return
const newEmployeeId = employeeResult.data.uuid
const workAddressValues = workAddress.form.getFormSubmissionValues()
await workAddress.actions.onSubmit(undefined, { employeeId: newEmployeeId })
await compensation.actions.onSubmit(undefined, {
employeeId: newEmployeeId,
startDate: workAddressValues?.effectiveDate,
})
},
)getFormSubmissionValues has no side effects — it doesn't trigger re-renders, mutate form state, or update validation errors. It's a pure read from react-hook-form's internal store followed by Zod schema parsing.
Each hook exposes form.hookFormInternals which provides direct access to the underlying react-hook-form formMethods (UseFormReturn). This is an escape hatch for advanced use cases that aren't covered by the hook's built-in API.
const { formMethods } = employeeDetails.form.hookFormInternals
formMethods.watch('email')
formMethods.setValue('firstName', 'Jane')
formMethods.trigger('ssn')Use this when you need to:
- Watch specific fields for reactive UI updates outside of the SDK fields
- Programmatically set or reset field values
- Trigger validation on specific fields manually
- Access form state like
isDirty,isValid, ordirtyFields
In most cases the built-in Fields, onSubmit, and getFormSubmissionValues are sufficient. Reach for hookFormInternals only when you need fine-grained form control that the hook doesn't expose directly.
Each hook exposes form.fieldsMetadata — an object keyed by field name with metadata about each field's current state. The field components consume this automatically under the hood to determine required/disabled states and populate select options, so you typically don't need to interact with it directly.
If you're building fully custom field UI, you can read this metadata yourself:
const { fieldsMetadata } = employeeDetails.form
if (fieldsMetadata.email.isRequired) {
// Show a required indicator in your custom UI
}