Hooks

useDocyrusFieldComponent

Resolve the right UI component (form input, value renderer, data-grid cell, editable value, or TanStack column def builder) for any Docyrus data source field type.

Installation

pnpm dlx @docyrus/cli add @docyrus/hooks-use-docyrus-field-component

This hook is distributed as source. It re-exports component registries used by DynamicFormField, DynamicValue, the data-grid cell system, and useDocyrusDataGrid, so adding a new field type only requires updating one place.

Overview

useDocyrusFieldComponent is a synchronous lookup hook that maps a Docyrus field type (e.g. field-select) to the right React component (or builder) for one of five render contexts:

  • form-field — TanStack Form input component (e.g. SelectFormField).
  • value-renderer — Read-only display component (e.g. SelectValue).
  • data-grid-cell-variant — TanStack-table cell component (e.g. SelectCell).
  • editable-value — Always returns EditableValue, which dispatches read/edit modes by field type internally.
  • tanstack-column-def — Returns a <TData>(opts) => ColumnDef<TData> builder that produces a fully-configured TanStack column for the field.

With this hook, you can build fully dynamic interfaces (auto-rendered forms, tables, and inline-edit views) just by iterating field metadata — no per-type switch statements at the call site.

Usage

'use client';

import { useDocyrusFieldComponent } from '@/hooks/use-docyrus-field-component';
import { type IField } from '@/components/docyrus/form-fields/types';

export function DynamicFieldRenderer({ field, value, record, form }: {
  field: IField;
  value: unknown;
  record: Record<string, unknown>;
  form: any;
}) {
  const FormField = useDocyrusFieldComponent(field.type, 'form-field');
  const Value     = useDocyrusFieldComponent(field.type, 'value-renderer');
  const Cell      = useDocyrusFieldComponent(field.type, 'data-grid-cell-variant');
  const Editable  = useDocyrusFieldComponent(field.type, 'editable-value');

  // form-field can be null for read-only/unsupported types
  if (FormField) return <FormField field={field} form={form} />;

  // value-renderer always returns a component (TextValue fallback)
  return <Value field={field} value={value} record={record} />;
}

The return type is conditionally typed by the kind argument:

const Form = useDocyrusFieldComponent('field-select', 'form-field');
//    ^? ComponentType<DocyrusFormFieldProps> | null

const Value = useDocyrusFieldComponent('field-select', 'value-renderer');
//    ^? ComponentType<DocyrusValueProps>

const Cell = useDocyrusFieldComponent('field-select', 'data-grid-cell-variant');
//    ^? ComponentType<DataGridCellProps<unknown>>

const Editable = useDocyrusFieldComponent('field-select', 'editable-value');
//    ^? typeof EditableValue

const Build = useDocyrusFieldComponent('field-select', 'tanstack-column-def');
//    ^? <TData>(opts: BuildTanstackColumnDefOptions) => ColumnDef<TData>

Building TanStack column defs

For dynamic tables, get a column-def builder for each field and feed the results to TanStack Table:

'use client';

import { useMemo } from 'react';
import { useDocyrusFieldComponent } from '@/hooks/use-docyrus-field-component';
import { useReactTable, getCoreRowModel } from '@tanstack/react-table';

export function ContactsTable({ fields, rows, appSlug, dataSourceSlug }) {
  const buildColumn = useDocyrusFieldComponent(fields[0]?.type, 'tanstack-column-def');

  const columns = useMemo(
    () => fields.map((field) => buildColumn({ field, appSlug, dataSourceSlug })),
    [fields, buildColumn, appSlug, dataSourceSlug]
  );

  const table = useReactTable({ data: rows, columns, getCoreRowModel: getCoreRowModel() });
  // ...
}

The builder returns ColumnDef<TData> with:

  • id = field.slug
  • accessorFn reads row[field.slug] and normalizes object payloads (enum/multi/relation) so grouping & sorting work
  • header = field.name
  • meta.label, meta.cell (the CellOpts config), meta.groupable, and meta.renderGroupValue (renders group headers with the right value renderer)

The fieldType argument is a memoization key for this kind — the builder itself reads field.type from the passed field, so a single builder can construct columns for any field type. Pass any IFieldType; we recommend the type of one representative field for stability.

API Reference

Parameters

ParameterTypeDescription
fieldTypeIFieldTypeThe Docyrus data source field type, e.g. field-text, field-select, field-dateTime.
kind'form-field' | 'value-renderer' | 'data-grid-cell-variant' | 'editable-value' | 'tanstack-column-def'Which render context to resolve the component for.

Return Value

kindReturnsUnknown-type fallback
form-fieldComponentType<DocyrusFormFieldProps> | nullnull (read-only/unsupported types)
value-rendererComponentType<DocyrusValueProps>TextValue
data-grid-cell-variantComponentType<DataGridCellProps<unknown>>ShortTextCell
editable-valuetypeof EditableValueEditableValue (it dispatches by field type internally)
tanstack-column-def<TData>(opts: BuildTanstackColumnDefOptions) => ColumnDef<TData>Builder produces short-text cell variant for unknown types

Exported registries

The hook module exports the underlying maps and helpers as named consts so non-React code, server components, or column-def builders can read them directly:

ExportTypePurpose
FORM_FIELD_MAPPartial<Record<IFieldType, ComponentType<DocyrusFormFieldProps>>>Field type → form input component.
VALUE_RENDERER_MAPPartial<Record<IFieldType, ComponentType<DocyrusValueProps>>>Field type → read-only renderer.
CELL_COMPONENT_MAPPartial<Record<IFieldType, ComponentType<DataGridCellProps<unknown>>>>Field type → data-grid cell component.
GROUPABLE_FIELD_TYPESSet<IFieldType>Field types selectable in the grouping picker.
getCellOpts(field, opts)(field, { appSlug?, dataSourceSlug? }) => CellOptsPure function: field metadata → TanStack meta.cell config.
buildTanstackColumnDef(opts)<TData>(opts) => ColumnDef<TData>Pure builder: field metadata → full ColumnDef.

DynamicFormField, DynamicValue, and useDocyrusDataGrid all consume this hook internally — adding a new field type means editing the maps in this module and nothing else.

BuildTanstackColumnDefOptions

FieldTypeDescription
fieldDocyrusFieldLikeField metadata. Accepts both IField and @docyrus/app-utils's DataSourceField.
appSlugstringWired into enum cell meta for dynamic option loading.
dataSourceSlugstringWired into enum cell meta for dynamic option loading.

How It Works

The hook is a memoized lookup with no state, no effects, and no fetching. It reads from one of the three exported registries based on kind, applies the appropriate fallback for unknown field types, and returns the resolved component (referentially stable while fieldType and kind are unchanged).

Why data-grid-cell-variant returns the cell component, not the variant string

The cell components (SelectCell, EnumCell, etc.) render correctly only when their host column is wired with the matching meta.cell config (options, app/data source slugs, etc.). For TanStack column-def construction with that wiring, use useDocyrusDataGrid, which builds the variant and the column meta together. This hook is the lower-level component-resolution primitive.

Why editable-value always returns EditableValue

EditableValue is itself a field-type dispatcher: internal sets (INLINE_TYPES, INSTANT_SAVE_TYPES, EXPLICIT_SAVE_TYPES, POPOVER_TYPES, READ_ONLY_TYPES) drive its read/edit behavior per field type. Returning the same component for every field type keeps the hook's API consistent (always returns a renderable component) and lets callers always render <Editable field={...} value={...} ... /> regardless of type.

Out of scope

  • Fetching enum options or relation records — pass them as props (enumOptions, etc.) when rendering the returned component.
  • Wiring up the full data-grid (rows, view tabs, toolbar) — use useDocyrusDataGrid which calls buildTanstackColumnDef internally.

On this page