Skip to main content

createControllableValue

Bridge controlled and uncontrolled component state with a single reactive accessor and setter pair.

View as Markdown

createControllableValue bridges controlled and uncontrolled component state. It allows custom form controls to seamlessly support both caller-managed values (props.value) and self-managed fallback values (props.defaultValue).

Import#

import { createControllableValue } from 'moraine/utils'

Signature#

function createControllableValue<T extends {} | null>(
  options: CreateControllableValueOptions<T>,
): readonly [Accessor<T>, (update: T | ((previous: T) => T)) => void]

interface CreateControllableValueOptions<T> {
  value: Accessor<T | undefined>
  defaultValue: Accessor<T>
}

Usage Example#

src/CustomToggle.tsx
import { createControllableValue } from 'moraine/utils'

interface CustomToggleProps {
  value?: boolean
  defaultValue?: boolean
  onChange?: (next: boolean) => void
}

export function CustomToggle(props: CustomToggleProps) {
  const [checked, setChecked] = createControllableValue({
    value: () => props.value,
    defaultValue: () => props.defaultValue ?? false,
  })

  function toggle() {
    const next = !checked()
    setChecked(next)
    props.onChange?.(next)
  }

  return (
    <button
      type="button"
      aria-pressed={checked()}
      onClick={toggle}
      class="px-3 py-1.5 rounded-md border"
    >
      {checked() ? 'Active' : 'Inactive'}
    </button>
  )
}

Behavior#

  • Controlled Mode: When options.value() returns a defined value, the returned accessor follows that value. Calling setValue will not mutate local state, but callers can intercept state in their event handlers.
  • Uncontrolled Mode: When options.value() returns undefined, the hook manages state internally starting with options.defaultValue().
  • Mode Switching: Mode changes between controlled and uncontrolled are tracked reactively.