react-formbridge
Browse documentation
Hooksv1.0.2

useFormBridgeAnalytics()

Add analytics to a form without rewriting any field component.

  • Track focus, completion time, change counts, abandonment, errors, and successful completion
  • Keep callbacks metadata-oriented so analytics stays safe and privacy-conscious
  • Works best when paired with a stable getter for current values
  • For most forms, passing analytics directly to useFormBridge(schema, { analytics }) is the simplest path; the standalone hook is the lower-level escape hatch
web.tsxtsx
1import {
2 field,
3 useFormBridge,
4 useFormBridgeAnalytics,
5} from '@runilib/react-formbridge'
6
7const schema = {
8 email: field.email('Email').required(),
9 password: field.password('Password').required(),
10}
11
12export function SignupWithAnalytics() {
13 const { Form, fields, state } = useFormBridge(schema)
14
15 useFormBridgeAnalytics(
16 {
17 formId: 'signup',
18 exclude: ['password'],
19 handlers: {
20 onFieldComplete: (name, ms) => analytics.track('field_complete', { name, ms }),
21 onFormCompleted: (durationMs, submitCount, fieldCount) =>
22 analytics.track('form_done', { durationMs, submitCount, fieldCount }),
23 onFormAbandoned: (pct, last, values) =>
24 analytics.track('form_abandoned', { pct, last, values }),
25 },
26 },
27 () => state.values,
28 )
29
30 return (
31 <Form onSubmit={(values) => api.signup(values)}>
32 <fields.email />
33 <fields.password />
34 <Form.Submit>Sign up</Form.Submit>
35 </Form>
36 )
37}

Config & defaults

Complete AnalyticsOptions surface (the first argument of useFormBridgeAnalytics()):

MethodTypeDescription
handlersAnalyticsHandlersRequired - set of callbacks the tracker fires on every tracked event. Each handler is individually optional
exclude?string[]Extra field names to exclude from every callback. Merged with the built-in deny-list ['password', 'confirm', 'cvv', 'pin', 'otp', 'ssn', 'secret']. Stripped from values / errors payloads passed to onFormAbandoned / onFormLevelError
formId?stringOptional identifier injected on the tracker instance. Pass-through only - the hook does not read it

Passing undefined as the whole config disables analytics: the previous tracker (if any) is destroyed, the hook returns null, and no listeners stay attached. Handy for a feature-flagged rollout.

Minimal config

AnalyticsConfig.tsts
1const analyticsConfig: AnalyticsOptions = {
2 formId: 'signup',
3 exclude: ['taxId', 'dateOfBirth'],
4 handlers: {
5 onFieldComplete: (name, ms) => track('field_complete', { name, ms }),
6 onFormCompleted: (ms, submitCount) => track('form_done', { ms, submitCount }),
7 },
8}

AnalyticsHandlers every callback is optional. The tracker skips excluded field names before invoking anything below.

Field lifecycle

MethodTypeDescription
onFieldFocus?(name) => voidFires when a tracked field gains focus. Records the focus timestamp (used later for durationMs). Does not fire for excluded fields
onFieldComplete?(name, durationMs) => voidFires on blur only when the blurred value is non-empty. durationMs = elapsed time between matching focus and this blur. Tracker then forgets the focus timestamp
onFieldAbandoned?(name, partialValue) => voidFires on blur when the value is empty after focus. Mutually exclusive with onFieldComplete for a given focus/blur pair
onFieldChange?(name, changeCount) => voidFires on every change. changeCount is a per-field counter to detect "hesitant" fields. No field value is ever passed (privacy)

Field validation

MethodTypeDescription
onFieldError?(name, error) => voidFires when a field enters an error state and the message differs from the previous one (dedup)
onFieldErrorFixed?(name) => voidFires exactly once when a previously-errored field becomes valid again

Form lifecycle

MethodTypeDescription
onFormAbandoned?(completedPercent, lastField, values) => voidFires at most once per tracker when the user leaves while partially filled (0 < completedPercent < 100). values is a filtered snapshot. Triggered by pagehide / beforeunload / visibilitychange on web, AppState transitions on native
onFormCompleted?(durationMs, submitCount, fieldCount) => voidFires once on successful submission. After this, the tracker is destroyed and no further events can be emitted
onFormLevelError?(errors, submitCount) => voidFires on every failed submit attempt. errors has excluded fields stripped out

Example - wiring every event to a single tracker

handlers.tsts
1const handlers: AnalyticsHandlers = {
2 onFieldFocus: (name) => track('field_focus', { name }),
3 onFieldComplete: (name, ms) => track('field_complete', { name, ms }),
4 onFieldAbandoned: (name) => track('field_abandoned', { name }),
5 onFieldChange: (name, count) => track('field_change', { name, count }),
6 onFieldError: (name, error) => track('field_error', { name, error }),
7 onFieldErrorFixed: (name) => track('field_error_fixed', { name }),
8 onFormAbandoned: (pct, lastField, values) =>
9 track('form_abandoned', { pct, lastField, fieldCount: Object.keys(values).length }),
10 onFormCompleted: (ms, submitCount, fieldCount) =>
11 track('form_done', { ms, submitCount, fieldCount }),
12 onFormLevelError: (errors, submitCount) => track('form_error', { errors, submitCount }),
13}

Returned tracker

useFormBridgeAnalytics(opts, getValues) returns a FormBridgeAnalyticsTracker | null:

  • Returns null when opts is undefined (analytics disabled).
  • Otherwise returns the live tracker instance so you can drive it imperatively from code that does not live inside useFormBridge() - useful when wiring a third-party field component that needs to notify the tracker manually.

When you pass analytics directly to useFormBridge(schema, { analytics }), you never touch this instance - the core wires every handler for you. The surface below only matters if you call the standalone hook as an escape hatch.

Core imperative methods

MethodTypeDescription
setValuesGetter(getter: () => Record<string, unknown>) => voidReplaces the function the tracker calls to get latest values (used before onFormAbandoned). The hook re-binds this on every render to a ref-backed getter
attachLifecycleTracking() => voidRegisters platform listeners (pagehide / beforeunload / visibilitychange on web, AppState.change on native). Idempotent
destroy() => voidRemoves every listener and marks the tracker detached. Called automatically on unmount and implicitly inside onFormSubmitSuccess()

Field events - call from a custom field integration

MethodTypeDescription
onFieldFocus(name) => voidRecords the focus timestamp and fires handlers.onFieldFocus
onFieldBlur(name, value) => voidComputes duration and fires onFieldComplete or onFieldAbandoned depending on emptiness
onFieldChange(name) => voidBumps per-field change counter and fires handlers.onFieldChange
onFieldError(name, error) => voidFires handlers.onFieldError only when the message differs from the previous one (dedup)
onFieldErrorFixed(name) => voidFires handlers.onFieldErrorFixed only if the field had a recorded error

Form events

MethodTypeDescription
onFormSubmitError(errors, submitCount) => voidFilters excluded keys out of errors and fires handlers.onFormLevelError
onFormSubmitSuccess(submitCount, fieldCount) => voidFires handlers.onFormCompleted with total duration since tracker start, then auto-destroys the tracker

Completion helpers

MethodTypeDescription
computeCompletion(values) => numberReturns rounded integer percentage of non-empty, non-excluded fields. Suitable for progress bars
reportAbandonmentIfNeeded() => voidSame logic as lifecycle listeners: if partially filled and not yet reported, fires handlers.onFormAbandoned. Fires at most once per tracker

Example - driving the tracker manually from a third-party field

CustomFieldWithAnalytics.tsxtsx
1const analytics = useFormBridgeAnalytics(
2 { formId: 'signup', handlers: myHandlers },
3 () => state.values,
4)
5
6return (
7 <ThirdPartySignaturePad
8 onFocus={() => analytics?.onFieldFocus('signature')}
9 onBlur={(value) => analytics?.onFieldBlur('signature', value)}
10 onChange={() => analytics?.onFieldChange('signature')}
11 />
12)

Usage notes

  • Pass a stable getter for current values, e.g. () => state.values
  • No field values are sent in onFieldChange; keep events metadata-only
  • Works on web (pagehide/beforeunload/visibilitychange) and React Native (AppState)