react-formbridge
Browse documentation
Hooksv2.0.0

useFormBridgeContext()

Consume the nearest form runtime from context.

  • Use it inside components rendered under <form.Form> when you want to split a form into reusable children without prop drilling
  • Use it with <form.FormProvider> when a toolbar, summary panel, sticky footer, or side layout lives outside the actual form wrapper
  • It returns the same runtime object as useFormBridge(schema) for the nearest provider
  • It throws if used outside <form.Form> or <form.FormProvider>
import {
  field,
  useFormBridge,
  useFormBridgeContext,
} from '@runilib/react-formbridge'

const schema = {
  email: field.email('Email').required(),
  marketingOptIn: field.checkbox('Receive product updates'),
}

function LiveSummary() {
  const { watch, state } = useFormBridgeContext<typeof schema>()
  const email = watch('email')

  return (
    <aside>
      <p>Current email: {email || '-'}</p>
      <p>{state.isValid ? 'Ready to submit' : 'Some required fields are still missing.'}</p>
    </aside>
  )
}

export function NewsletterSettings() {
  const form = useFormBridge(schema, {
    validateOn: 'onBlur',
    revalidateOn: 'onChange',
  })

  return (
    <form.Form onSubmit={async (values) => api.save(values)}>
      <AppField form={form} name="email" />
      <AppField form={form} name="marketingOptIn" />
      <LiveSummary />
      <button type="submit">Save preferences</button>
    </form.Form>
  )
}

function AppField({ form, name }) {
  const controller = form.fieldController(name)

  if (!controller.visible) return null

  if (controller.options?.length) {
    return (
      <label>
        {controller.label}
        <select
          value={String(controller.value ?? '')}
          disabled={controller.disabled}
          onChange={(event) => controller.onChange(event.target.value)}
          onBlur={controller.onBlur}
        >
          {controller.options.map((option) => (
            <option key={String(option.value)} value={option.value}>
              {option.label}
            </option>
          ))}
        </select>
        {controller.error ? <span role="alert">{controller.error}</span> : null}
      </label>
    )
  }

  if (typeof controller.value === 'boolean') {
    return (
      <label>
        <input
          type="checkbox"
          checked={controller.value}
          disabled={controller.disabled}
          onChange={(event) => controller.onChange(event.target.checked)}
          onBlur={controller.onBlur}
        />
        {controller.label}
      </label>
    )
  }

  return (
    <label>
      {controller.label}
      <input
        value={controller.displayValue ?? String(controller.value ?? '')}
        placeholder={controller.placeholder}
        disabled={controller.disabled}
        onChange={(event) => controller.onChange(event.target.value)}
        onBlur={controller.onBlur}
      />
      {controller.error ? <span role="alert">{controller.error}</span> : null}
    </label>
  )
}

const api = new Proxy({}, {
  get: (_target, methodName) => async (values) => {
    console.log('[doc-playground]', String(methodName), values)
    return { methodName, values }
  },
})

export default NewsletterSettings

Use FormProvider outside the form tree

<form.Form> already provides context to everything rendered inside it. Reach for <form.FormProvider> only when a consumer is rendered outside the actual form wrapper but still needs access to submit(), state, watch(), or fieldController().

import {
  field,
  useFormBridge,
  useFormBridgeContext,
} from '@runilib/react-formbridge'

const schema = {
  email: field.email('Email').required(),
  coupon: field.text('Coupon code'),
}

function StickyCheckoutBar() {
  const { state, submit } = useFormBridgeContext<typeof schema>()

  return (
    <footer>
      <p>{state.isValid ? 'Ready to pay' : 'Complete the required fields first'}</p>
      <button
        type="button"
        disabled={state.isSubmitting}
        onClick={() => void submit()}
      >
        Pay now
      </button>
    </footer>
  )
}

export function CheckoutLayout() {
  const form = useFormBridge(schema, {
    persist: { key: 'checkout-layout' },
  })

  return (
    <form.FormProvider>
      <div className="checkout-layout">
        <main>
          <form.Form onSubmit={async (values) => api.checkout(values)}>
            <AppField form={form} name="email" />
            <AppField form={form} name="coupon" />
          </form.Form>
        </main>

        <StickyCheckoutBar />
      </div>
    </form.FormProvider>
  )
}

function AppField({ form, name }) {
  const controller = form.fieldController(name)

  if (!controller.visible) return null

  if (controller.options?.length) {
    return (
      <label>
        {controller.label}
        <select
          value={String(controller.value ?? '')}
          disabled={controller.disabled}
          onChange={(event) => controller.onChange(event.target.value)}
          onBlur={controller.onBlur}
        >
          {controller.options.map((option) => (
            <option key={String(option.value)} value={option.value}>
              {option.label}
            </option>
          ))}
        </select>
        {controller.error ? <span role="alert">{controller.error}</span> : null}
      </label>
    )
  }

  if (typeof controller.value === 'boolean') {
    return (
      <label>
        <input
          type="checkbox"
          checked={controller.value}
          disabled={controller.disabled}
          onChange={(event) => controller.onChange(event.target.checked)}
          onBlur={controller.onBlur}
        />
        {controller.label}
      </label>
    )
  }

  return (
    <label>
      {controller.label}
      <input
        value={controller.displayValue ?? String(controller.value ?? '')}
        placeholder={controller.placeholder}
        disabled={controller.disabled}
        onChange={(event) => controller.onChange(event.target.value)}
        onBlur={controller.onBlur}
      />
      {controller.error ? <span role="alert">{controller.error}</span> : null}
    </label>
  )
}

const api = new Proxy({}, {
  get: (_target, methodName) => async (values) => {
    console.log('[doc-playground]', String(methodName), values)
    return { methodName, values }
  },
})

export default CheckoutLayout

Return value

The hook returns the same runtime surface as the nearest useFormBridge(schema) call.

Complete hook return surface. Every value listed below is returned by useFormBridge() in the exact order shown in the code snippet above, with a link to its dedicated reference section:

MethodTypeDescription
FormProviderComponentTypeAdvanced context wrapper for consumers rendered outside <Form>
FormComponentTypeMinimal wrapper connected to the submit lifecycle
FieldErrorComponentType<{ name }>Standalone error renderer for one field name
FieldLabelComponentType<{ name }>Standalone label renderer for one field name
fieldController(name) => ControllerField-scoped runtime for fully custom UI while keeping the schema contract
stateFormStateReactive FormState object (values, errors, touched, dirty, isValid, isSubmitting, …)
visibilityRecord<name, Flags>Per-field visibility / required / disabled state computed from conditional rules
persistanceHelpersobjectisLoadingDraft, hasDraft, clearDraft(), and saveDraftNow()
setValue(name, value) => voidSet one field value programmatically
getValue(name) => valueRead one field value
getValues() => ValuesRead the full value object
validate(names?) => Promise<boolean>Validate one field, several fields, or the full form
resetFields(values?) => voidReset to schema defaults or a provided partial value object
setError(name, message) => voidPush a manual field error
clearErrors(name?) => voidClear one field, many fields, or all errors
watch(name) => valueReactive single-field read
watchAll() => ValuesReactive full-value read
submit() => Promise<void>Imperative submit using the validation and submit pipeline
handleSubmit(event?) => Promise<void>Event-friendly submit handler for an application-owned web form

Rules & composition notes

  • The hook must run below the nearest <form.Form> or <form.FormProvider>
  • The nearest provider wins, so nested form providers isolate their own runtime
  • Prefer plain props when one small child needs one value; prefer context when several descendants need coordinated access to the form runtime
  • watch(), watchAll(), state, and visibility remain reactive because the provider shares the live form api object
  • If a consumer needs to trigger submission from outside the rendered form element, <form.FormProvider> is the intended path