Skip to main content
While valued components provide powerful two-way data binding, there are scenarios where you only need to display data without allowing user modifications. One-way data binding enables components to read from form data without writing back to it, making them perfect for read-only displays, calculated fields, and data presentation.

What Is One-Way Data Binding?

One-way data binding allows components to:
  • ✅ Read data from the form store
  • ❌ Write data back to the form store
  • ✅ React to data changes automatically
  • ✅ Display computed or derived values
  • ✅ Show read-only representations of form data
Common Use Cases:
  • Read-only text displays
  • Calculated fields (totals, averages, derived values)
  • Data visualization components
  • Summary panels
  • Status indicators
  • Preview components

One-Way vs Two-Way Binding

FeatureOne-Way BindingTwo-Way Binding (Valued)
Reads from store✅ Yes✅ Yes
Writes to store❌ No✅ Yes
User can modify❌ No✅ Yes
Triggers validation❌ No✅ Yes
Tracks dirty state❌ No✅ Yes
Uses dataBound✅ Yes❌ No
Uses valued❌ No✅ Yes

Creating One-Way Bound Components

One-way bound components use the dataBound method to create read-only connections to form data. Unlike valued, dataBound props receive data from the store but cannot write back to it.

Example: Read-Only Display Component


interface DisplayProps {
  /** The value to display */
  value: string;

  /** Optional label */
  label?: string;

  /** Formatting variant */
  variant?: 'text' | 'email' | 'phone' | 'currency';
}

const ReadOnlyDisplay = ({value, label, variant}: DisplayProps) => {
  const formatValue = (val: string) => {
    switch (variant) {
      case 'email':
        return <a href={`mailto:${val}`}>{val}</a>
      case 'phone':
        return <a href={`tel:${val}`}>{val}</a>
      case 'currency':
        return new Intl.NumberFormat('en-US', {
          style: 'currency',
          currency: 'USD'
        }).format(Number(val))
      default:
        return val
    }
  }

  return (
    <div className="read-only-display">
      {label && <label className="read-only-display__label">{label}</label>}
      <div className="read-only-display__value">
        {formatValue(value)}
      </div>
    </div>
  )
}

export const readOnlyDisplay = define(ReadOnlyDisplay, 'ReadOnlyDisplay')
  .props({
    value: string.dataBound, // Use dataBound for one-way binding!
    label: string,
    variant: string.default('text')
  })
  .build()

Using the Read-Only Component

{
  "key": "userEmailDisplay",
  "dataKey": "userEmail",
  "type": "ReadOnlyDisplay",
  "props": {
    "label": {
      "value": "User Email"
    },
    "variant": {
      "value": "email"
    }
  }
}
Key Points:
  • The value prop uses dataBound to receive data from the form
  • No onChange handler is needed or provided
  • The component re-renders automatically when form.data.userEmail changes
  • Users cannot edit the displayed value
  • No validation or dirty state tracking occurs

Live Example

live
function App() {
  const ReadOnlyDisplay = ({value, label, variant}) => {
    const formatValue = (val) => {
      switch (variant) {
        case 'email':
          return <a href={`mailto:${val}`}>{val}</a>
        case 'phone':
          return <a href={`tel:${val}`}>{val}</a>
        case 'currency':
          return new Intl.NumberFormat('en-US', {
            style: 'currency',
            currency: 'USD'
          }).format(Number(val))
        default:
          return val
      }
    }

    return (
      <div className="read-only-display">
        {label && <label className="read-only-display__label">{label}</label>}
        <div className="read-only-display__value">
          {formatValue(value)}
        </div>
      </div>
    )
  }

  const readOnlyDisplay = define(ReadOnlyDisplay, 'ReadOnlyDisplay')
    .props({
      value: string.dataBound,
      label: string,
      variant: string.default('text')
    })
    .build()

  const view = muiView
  muiView.define(readOnlyDisplay.model)

  const formJson = {
    "form": {
      "key": "Screen",
      "type": "Screen",
      "children": [
        {
          "key": "userEmailDisplay",
          "dataKey": "userEmail",
          "type": "ReadOnlyDisplay",
          "props": {
            "label": {
              "value": "User Email"
            },
            "variant": {
              "value": "email"
            }
          }
        },
        {
          "key": "textField",
          "type": "MuiTextField",
          "dataKey": "userEmail",
          "props": {
            "helperText": {
              "value": "Enter your email"
            }
          }
        }
      ]
    }
  }

  return (
    <FormViewer
      view={view}
      initialData={{userEmail: 'test@example.com'}}
      getForm={() => JSON.stringify(formJson)}
    />
  )
}

Real-World Examples

Example 1: Progress Indicator


interface ProgressProps {
  current: number
  total: number
}

const ProgressIndicator = ({current, total}: ProgressProps) => {
  const percentage = total > 0 ? (current / total) * 100 : 0

  return (
    <div className="progress-indicator">
      <div className="progress-indicator__bar">
        <div
          className="progress-indicator__fill"
          style={{width: `${percentage}%`}}
        />
      </div>
      <div className="progress-indicator__text">
        {current} of {total} completed ({percentage.toFixed(1)}%)
      </div>
    </div>
  )
}

export const progressIndicator = define(ProgressIndicator, 'ProgressIndicator')
  .props({
    current: number.dataBound.default(0),
    total: number.default(100)
  })
  .build()
Usage:
{
  "key": "progressIndicator",
  "dataKey": "progressIndicator",
  "type": "ProgressIndicator"
}

Live Example

live
function App() {
  const ProgressIndicator = ({current, total}) => {
    const percentage = total > 0 ? (current / total) * 100 : 0

    return (
      <div className="progress-indicator">
        <div className="progress-indicator__bar">
          <div
            className="progress-indicator__fill"
            style={{width: `${percentage}%`}}
          />
        </div>
        <div className="progress-indicator__text">
          {current} of {total} completed ({percentage.toFixed(1)}%)
        </div>
      </div>
    )
  }

  const progressIndicator = define(ProgressIndicator, 'ProgressIndicator')
    .props({
      current: number.dataBound.default(0),
      total: number.default(100)
    })
    .build()

  const view = muiView
  muiView.define(progressIndicator.model)

  const formJson = {
    "form": {
      "key": "Screen",
      "type": "Screen",
      "children": [
        {
          "key": "progressIndicator",
          "dataKey": "progressIndicator",
          "type": "ProgressIndicator"
        }
      ]
    }
  }

  return (
    <FormViewer
      view={view}
      initialData={{progressIndicator: 24}}
      getForm={() => JSON.stringify(formJson)}
    />
  )
}

Example 2: Data Visualization Component


interface ChartProps {
  data: Array<{ label: string; value: number }>
  title?: string
}

const SimpleBarChart = ({data, title}: ChartProps) => {
  const items = data ?? []
  const maxValue = Math.max(...items.map(d => d.value), 1)

  return (
    <div className="simple-chart">
      {title && <h4>{title}</h4>}
      <div className="simple-chart__bars" style={{maxWidth: 200}}>
        {items.map((item, index) => (
          <div
            key={index}
            className="simple-chart__bar-container"
            style={{
              width: `${(item.value / maxValue) * 100}%`,
              backgroundColor: `hsl(${index * 40}, 70%, 50%)`
            }}
            title={`${item.label}: ${item.value}`}
          >
            <span className="simple-chart__label">{item.label}</span>
          </div>
        ))}
      </div>
    </div>
  )
}

export const simpleBarChart = define(SimpleBarChart, 'SimpleBarChart')
  .props({
    data: array.dataBound.default([]),
    title: string,
    type: string.default('bar')
  })
  .build()
Usage:
{
  "key": "salesChart",
  "dataKey": "salesData",
  "type": "SimpleBarChart",
  "props": {
    "title": {
      "value": "Monthly Sales"
    }
  }
}

Live Example

live
function App() {
  const SimpleBarChart = ({data, title}) => {
    const items = data ?? []
    const maxValue = Math.max(...items.map(d => d.value), 1)

    return (
      <div className="simple-chart">
        {title && <h4>{title}</h4>}
        <div className="simple-chart__bars" style={{maxWidth: 200}}>
          {items.map((item, index) => (
            <div
              key={index}
              className="simple-chart__bar-container"
              style={{
                width: `${(item.value / maxValue) * 100}%`,
                backgroundColor: `hsl(${index * 40}, 70%, 50%)`
              }}
              title={`${item.label}: ${item.value}`}
            >
              <span className="simple-chart__label">{item.label}</span>
            </div>
          ))}
        </div>
      </div>
    )
  }

  const simpleBarChart = define(SimpleBarChart, 'SimpleBarChart')
    .props({
      data: array.dataBound.default([]),
      title: string,
      type: string.default('bar')
    })
    .build()

  const view = muiView
  muiView.define(simpleBarChart.model)

  const formJson = {
    "form": {
      "key": "Screen",
      "type": "Screen",
      "children": [
        {
          "key": "salesChart",
          "dataKey": "salesData",
          "type": "SimpleBarChart",
          "props": {
            "title": {
              "value": "Monthly Sales"
            }
          }
        }
      ]
    }
  }

  const initialData = {
    salesData: [
      {
        label: 'Item 1',
        value: 56
      },
      {
        label: 'Item 2',
        value: 23
      },
      {
        label: 'Item 3',
        value: 14
      }
    ]
  }

  return (
    <FormViewer
      view={view}
      initialData={initialData}
      getForm={() => JSON.stringify(formJson)}
    />
  )
}

Best Practices

1. Use One-Way Binding for Read-Only Data

When a component should only display information, use dataBound to prevent accidental data modification and reduce complexity. Good:
// Read-only display with dataBound

const UserInfo = ({info}: any) => (
  <div>
    <p>{info?.name}</p>
    <p>{info?.email}</p>
  </div>
)

export const userInfo = define(UserInfo, 'UserInfo')
  .props({
    info: object.dataBound
  })
  .build()
Avoid:
// Don't use valued props if you don't need two-way binding

const UserInfo = ({info}: any) => (
  <div>
    <p>{info?.name}</p>
    <p>{info?.email}</p>
  </div>
)

export const userInfo = define(UserInfo, 'UserInfo')
  .props({
    info: object.valued // Wrong: enables two-way binding unnecessarily
  })
  .build()

2. Combine with Valued Components

One-way and two-way components work together seamlessly:
{
  "form": {
    "key": "Screen",
    "type": "Screen",
    "children": [
      {
        "key": "name",
        "type": "MuiTextField",
        "props": {
          "helperText": {
            "value": "Enter your name"
          },
          "label": {
            "value": "Name"
          }
        }
      },
      {
        "key": "muiTypography1",
        "type": "MuiTypography",
        "props": {
          "children": {
            "value": "Your name is:"
          },
          "variant": {
            "value": "h5"
          }
        }
      },
      {
        "key": "muiTypography2",
        "dataKey": "name",
        "type": "MuiTypography",
        "props": {
          "variant": {
            "value": "h5"
          }
        }
      }
    ]
  }
}

Live Example

live
function App() {
  const formJson = {
    "form": {
      "key": "Screen",
      "type": "Screen",
      "children": [
        {
          "key": "name",
          "type": "MuiTextField",
          "props": {
            "helperText": {
              "value": "Enter your name"
            },
            "label": {
              "value": "Name"
            }
          }
        },
        {
          "key": "muiTypography1",
          "type": "MuiTypography",
          "props": {
            "children": {
              "value": "Your name is:"
            },
            "variant": {
              "value": "h5"
            }
          }
        },
        {
          "key": "muiTypography2",
          "dataKey": "name",
          "type": "MuiTypography",
          "props": {
            "variant": {
              "value": "h5"
            }
          }
        }
      ]
    }
  }

  return (
    <FormViewer
      view={muiView}
      initialData={{name: 'Michael'}}
      getForm={() => JSON.stringify(formJson)}
    />
  )
}

3. Performance Benefits

One-way bound components are more performant because:
  • No validation overhead
  • No dirty state tracking
  • No event handlers for data updates
  • Simpler re-render logic
  • Explicit read-only contract with dataBound

Summary

One-way data binding with dataBound is a powerful pattern for creating read-only components that react to form data changes. Use it when:
  • Displaying computed or derived values
  • Creating summary panels
  • Building data visualization components
  • Showing read-only representations of form data
  • Implementing progress indicators or status displays
Remember: one-way components use dataBound to receive data explicitly as read-only, don’t provide onChange handlers, and are optimized for presentation without the overhead of two-way binding.
Last modified on April 16, 2026