react-formbridge
Browse documentation
Hooksv1.0.2

useFormBridgeReadonly()

Render schema-driven values as readonly rows or as a diff against original values.

  • Useful for review steps before submission, audit views, change approval screens, or before/after comparisons
  • It reuses the schema labels and option metadata, so your review UI stays aligned with your editing UI
import { useState } from 'react'
import {
  field,
  useFormBridge,
  useFormBridgeReadonly,
} from '@runilib/react-formbridge'

const schema = {
  fullName: field.text('Full name').required(),
  email: field.email('Email').required(),
  country: field
    .select('Country')
    .options([
      { label: 'France', value: 'FR' },
      { label: 'United States', value: 'US' },
      { label: 'United Kingdom', value: 'GB' },
    ])
    .required(),
  newsletter: field.checkbox('Receive product updates'),
}

const originalValues = {
  fullName: 'Ava Martin',
  email: 'ava@runilib.dev',
  country: 'FR',
  newsletter: true,
}

const editedValues = {
  fullName: 'Ava Martin',
  email: 'ava.martin@runilib.dev',
  country: 'GB',
  newsletter: false,
}

const rowStyle = {
  border: '1px solid #d6d9e0',
  borderRadius: 12,
  padding: 12,
  background: '#fff',
}

export function ReadonlyPlayground() {
  const [mode, setMode] = useState<'readonly' | 'diff'>('diff')
  const [submitted, setSubmitted] = useState<Record<string, unknown> | null>(null)

  const form = useFormBridge(schema, {
    validateOn: 'onBlur',
    initialValues: originalValues,
  })

  const { Form, fields, state } = form

  const readonly = useFormBridgeReadonly(schema, {
    mode,
    values: state.values,
    originalValues,
  })

  const { changedFields, fieldNames, fields: previewFields, hasChanges } = readonly

  return (
    <div
      style={{
        fontFamily: 'sans-serif',
        padding: 20,
        background: '#f5f7fb',
        display: 'grid',
        gap: 16,
      }}
    >
      <div>
        <h3 style={{ margin: '0 0 8px' }}>Edit profile + readonly preview</h3>
        <p style={{ margin: 0, color: '#4b5563' }}>
          The form stays editable, while the preview reuses the same schema labels,
          select labels, and diff metadata.
        </p>
      </div>

      <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
        <button type="button" onClick={() => form.resetFields(editedValues)}>
          Load sample edits
        </button>
        <button type="button" onClick={() => form.resetFields(originalValues)}>
          Reset to original
        </button>
        <button
          type="button"
          onClick={() =>
            setMode((current) => (current === 'diff' ? 'readonly' : 'diff'))
          }
        >
          Toggle {mode === 'diff' ? 'readonly' : 'diff'} preview
        </button>
      </div>

      <div
        style={{
          display: 'grid',
          gridTemplateColumns: 'minmax(0, 1fr) minmax(0, 1fr)',
          gap: 16,
          alignItems: 'start',
        }}
      >
        <Form
          onSubmit={async (values) => {
            setSubmitted(values)
          }}
        >
          <div style={{ display: 'grid', gap: 12 }}>
            <h4 style={{ margin: 0 }}>Editable form</h4>
            <fields.fullName />
            <fields.email />
            <fields.country />
            <fields.newsletter />
            <Form.Submit>Save profile</Form.Submit>
          </div>
        </Form>

        <aside style={{ display: 'grid', gap: 10 }}>
          <div>
            <h4 style={{ margin: '0 0 4px' }}>Readonly preview</h4>
            <p style={{ margin: 0, color: '#4b5563', fontSize: 13 }}>
              Mode: <strong>{mode}</strong>
              {' · '}
              {hasChanges
                ? `${changedFields.length} changed field(s)`
                : 'No detected changes'}
            </p>
          </div>

          {fieldNames.map((name) => {
            const preview = previewFields[name]

            return (
              <div
                key={preview.name}
                style={{
                  ...rowStyle,
                  borderColor:
                    mode === 'diff' && preview.changed ? '#38bdf8' : '#d6d9e0',
                }}
              >
                <div
                  style={{
                    display: 'flex',
                    justifyContent: 'space-between',
                    gap: 8,
                  }}
                >
                  <strong>{preview.label}</strong>
                  {mode === 'diff' && preview.changed ? (
                    <span style={{ color: '#0369a1', fontSize: 12 }}>Edited</span>
                  ) : null}
                </div>

                <p style={{ margin: '8px 0 0' }}>{preview.display}</p>

                {mode === 'diff' && preview.changed ? (
                  <p
                    style={{
                      margin: '6px 0 0',
                      color: '#64748b',
                      fontSize: 13,
                    }}
                  >
                    Original: {preview.originalDisplay}
                  </p>
                ) : null}
              </div>
            )
          })}
        </aside>
      </div>

      <div
        style={{
          border: '1px solid #d6d9e0',
          borderRadius: 12,
          padding: 12,
          background: '#fff',
        }}
      >
        <strong>Last submit</strong>
        <pre style={{ marginBottom: 0, whiteSpace: 'pre-wrap' }}>
          {JSON.stringify(submitted, null, 2)}
        </pre>
      </div>
    </div>
  )
}

export default ReadonlyPlayground

Options

  • First argument: the schema

Complete useFormBridgeReadonly() options surface:

MethodTypeDescription
mode'readonly' | 'diff''readonly' renders plain read-only rows. 'diff' highlights fields whose values[name] differs from originalValues[name] and exposes the before/after pair
valuesSchemaValues<S>Current values to render - shape comes directly from your schema, so every key is typed
originalValues?Partial<SchemaValues<S>>Baseline values used in diff mode to compute changed / changedFields. Ignored in readonly mode. Fields absent from this map are never flagged as changed
formatters?Partial<Record<keyof S, (value) => string>>Per-field display formatter. Overrides the built-in formatting (dates → toLocaleDateString(), booleans → ✓ Yes / ✗ No, passwords → ••••••••, select/radio → matching option label, empty → -)

Return

Each fields[name] entry (FieldReadonlyState) contains:

MethodTypeDescription
namestringThe schema key
labelstringSame label as in the editing form (descriptor._label ?? "")
valueunknownRaw value from options.values
displaystringFormatted, ready-to-render string (custom formatter wins over the built-in one)
changedbooleantrue only in diff mode when the field has an entry in originalValues and it differs from value (via Object.is)
original?unknownRaw originalValues[name] - present only when changed is true
originalDisplay?stringFormatted version of original - useful for rendering a "before" column. Present only when changed is true

Each generated ReadonlyFields.name(props?) component accepts:

MethodTypeDescription
label?stringPer-render label override. Falls back to the schema label
format?(value) => stringOne-off formatter for this render only. Takes precedence over options.formatters[name] and the built-in formatting
style?objectCross-platform inline style forwarded to the readonly renderer's root element
className?stringForwarded className for the web readonly renderer (ignored on native)
showDiff?booleanForce the before/after UI on or off for this render. Defaults to options.mode === 'diff'

Complete useFormBridgeReadonly() return surface:

MethodTypeDescription
fieldsRecord<keyof S, FieldReadonlyState>Computed readonly state for every visible (non-_hidden) field - see the field state table above
fieldNamesArray<keyof S>Visible field names in schema iteration order - use this to render rows deterministically instead of Object.keys(fields)
changedFieldsArray<keyof S>Subset of fieldNames whose changed flag is true. Always empty in readonly mode
hasChangesbooleanShorthand for changedFields.length > 0. Handy for "Nothing changed" empty states
ReadonlyFields{ [K in keyof S]: (props?) => ReactElement | null }One ready-to-render component per schema key, reading from fields[name]

Platform note

Readonly review flows are currently most battle-tested on web. If you plan to rely on this API in native screens too, validate the exact renderer behavior you need before rolling it out broadly.