Use the bridge option when your real validation source of truth already lives in Zod, Yup, Valibot, Joi, or another schema library.
- The schema builders still drive rendering and UX metadata
- The external schema owns the final values/errors decision
- Successful parsed/coerced values are now forwarded to submit handlers
- This is often the cleanest path in domains that already share validation with the backend
zod-bridge.tsts
| 1 | import { z } from 'zod' |
| 2 | import { field, useFormBridge, zodBridge } from '@runilib/react-formbridge' |
| 3 | |
| 4 | const schema = { |
| 5 | email: field.email('Email').required(), |
| 6 | password: field.password('Password').required(), |
| 7 | } |
| 8 | |
| 9 | const zodSchema = z.object({ |
| 10 | email: z.string().email(), |
| 11 | password: z.string().min(8), |
| 12 | }) |
| 13 | |
| 14 | const form = useFormBridge(schema, { |
| 15 | validatorBridge: zodBridge(zodSchema), |
| 16 | }) |
Shared customization options
All four built-in adapters share the same customization surface, so you can keep the same mental model even if the schema library changes later.
Shared adapter options (BridgeAdapterOptions) supported by all built-in bridges:
| Method | Type | Description |
|---|---|---|
rootKey? | string | null | Where pathless errors land. Default '_root'. Set null to drop them |
errorMode? | 'first' | 'join' | 'last' | How duplicate field errors are aggregated |
joinMessagesWith? | string | Separator for errorMode: 'join' |
formatPath? | (path, issue) => string | Rewrite the final error key |
mapIssue? | (context) => Issue | null | Remap, skip, or rewrite an issue before it hits the error bag |
normalizeMessage? | (message, issue) => string | Final message normalization hook |
bridge-options.tsts
| 1 | import { field, joiBridge, useFormBridge } from '@runilib/react-formbridge' |
| 2 | import Joi from 'joi' |
| 3 | |
| 4 | const schema = { |
| 5 | email: field.email('Email').required(), |
| 6 | plan: field.select('Plan').required(), |
| 7 | } |
| 8 | |
| 9 | const joiSchema = Joi.object({ |
| 10 | email: Joi.string().email({ tlds: false }).required(), |
| 11 | plan: Joi.string().required(), |
| 12 | }) |
| 13 | |
| 14 | const form = useFormBridge(schema, { |
| 15 | validatorBridge: joiBridge(joiSchema, { |
| 16 | rootKey: 'form', |
| 17 | errorMode: 'join', |
| 18 | joinMessagesWith: ' · ', |
| 19 | formatPath: (path) => path.map(String).join('.'), |
| 20 | normalizeMessage: (message) => message.trim(), |
| 21 | mapIssue: ({ defaultMessage, defaultPathKey }) => { |
| 22 | if (defaultPathKey === 'plan') { |
| 23 | return { message: `Billing: ${defaultMessage}` } |
| 24 | } |
| 25 | |
| 26 | return undefined |
| 27 | }, |
| 28 | }), |
| 29 | }) |
Library-specific options
Each adapter also exposes the options you usually need from its schema engine.
Library-specific bridge options:
| Method | Type | Description |
|---|---|---|
zodBridge | (schema, { mode?, parseOptions?, ...shared }) | mode?: 'auto' | 'sync' | 'async' + Zod parseOptions |
yupBridge | (schema, { mode?, validateOptions?, ...shared }) | mode?: 'auto' | 'sync' | 'async' + Yup validateOptions |
joiBridge | (schema, { mode?, validateOptions?, stripQuotes?, ...shared }) | mode?: 'auto' | 'sync' | 'async' + Joi validateOptions + stripQuotes |
valibotBridge | (schema, { mode?, parseOptions?, module?, ...shared }) | mode?: 'auto' | 'sync' | 'async' + Valibot parseOptions + module |
mode accepts 'auto', 'sync', or 'async'. The default 'auto' picks the async method when available, then falls back to sync. For Valibot, pass module: v if you want to avoid relying on runtime require(), especially in stricter ESM/browser setups.
Tips
| Tip | Details |
|---|---|
| Bridge contract | Must return { values, errors } - the built-in adapters already handle this for you |
| Prefer built-ins | Customize an existing adapter before writing a custom bridge from scratch |
| Root errors | Default to '_root' - useful for banner-level or submit-level failures |
| Cross-platform | A bridge works with the same useFormBridge() API on web and native |
| When to reach for a bridge | Business validation already exists elsewhere - otherwise prefer builder rules |
| Valibot specifics | Expects valibot installed in the consumer app, or passed explicitly via module: v |