Vitus Labs
Attrs

Chain Methods

Complete reference for attrs chaining API — .attrs(), .config(), .compose(), .statics(), .getDefaultAttrs().

.attrs(param, config?)

Add default props to the component. Can be called multiple times — defaults stack left-to-right (later calls override earlier ones).

Signature

.attrs<P extends TObj | unknown = unknown>(
  param: Partial<Props> | ((props: Partial<Props>) => Partial<Props>),
  config?: {
    priority?: boolean
    filter?: string[]
  }
): AttrsComponent

Object Form

Static default props merged into the component:

const Button = attrs({ name: 'Button', component: Element })
  .attrs({ tag: 'button', className: 'btn' })
  .attrs({ alignX: 'center', alignY: 'center' })

Objects are internally normalized to callbacks: { tag: 'button' } becomes () => ({ tag: 'button' }).

Callback Form

Compute defaults dynamically based on current props:

const Button = attrs({ name: 'Button', component: Element })
  .attrs((props) => ({
    'aria-label': props.label || 'unnamed',
    'aria-disabled': props.disabled === true,
    tabIndex: props.disabled ? -1 : 0,
  }))

The callback receives merged props (priority attrs + earlier attrs + explicit props, with undefined values stripped).

Config Options

.attrs(param, {
  priority?: boolean,
  filter?: string[],
})
OptionDefaultDescription
priorityfalseWhen true, these attrs resolve first (lower precedence than normal attrs and explicit props)
filter[]Prop names to strip before reaching the underlying component

Priority Attrs

Priority attrs are resolved before normal attrs. They provide a baseline that can be overridden by both normal attrs and explicit props:

const Input = attrs({ name: 'Input', component: Element })
  .attrs({ tabIndex: 0 }, { priority: true })  // baseline
  .attrs({ tag: 'input' })                      // normal

// tabIndex=0 unless overridden by explicit props or normal attrs

Merge order:

  1. Priority attrs callbacks called with filtered explicit props
  2. Normal attrs callbacks called with { ...priorityResult, ...filteredExplicitProps }
  3. Final: { ...priorityResult, ...normalResult, ...filteredExplicitProps }

Filtered Props

Strip internal-only props that shouldn't reach the DOM:

const Card = attrs({ name: 'Card', component: Element })
  .attrs(
    (props) => ({
      className: props.variant === 'elevated' ? 'shadow-lg' : '',
    }),
    { filter: ['variant'] }  // variant won't reach Element
  )

<Card variant="elevated" />
// Element receives { className: 'shadow-lg' } — no variant prop

Filters accumulate across chain calls — each .attrs() with a filter adds to the list.

Type Extension

Use the generic parameter to extend the prop type:

const Button = attrs({ name: 'Button', component: Element })
  .attrs<{ variant: 'primary' | 'secondary' }>({
    variant: 'primary',
    tag: 'button',
  })
  .attrs<{ size?: 'sm' | 'md' | 'lg' }>({})

// TypeScript accumulates: Element props + { variant } + { size? }

Multiple Chains

Each .attrs() call appends to the chain. Later callbacks can reference values set by earlier ones:

const Button = attrs({ name: 'Button', component: Element })
  .attrs(() => ({ color: 'blue' }))
  .attrs(() => ({ size: 'lg' }))
  .attrs((props) => ({
    // Can access props.color and props.size from earlier chains
    className: `${props.color}-${props.size}`,
  }))

.config(options)

Update the component's configuration — name, base component, or debug mode.

.config({
  name?: string,
  component?: ElementType,
  DEBUG?: boolean,
}): AttrsComponent
ParameterTypeDescription
namestringUpdates displayName. If omitted, keeps existing name.
componentElementTypeReplaces the wrapped component. Props type (OA) re-extracted from new component.
DEBUGbooleanDebug flag (reserved for future use).

Rename

const Primary = Button.config({ name: 'PrimaryButton' })
// React DevTools shows "PrimaryButton"
// data-attrs="PrimaryButton" in development

Swap Component

Replace the underlying component while keeping all accumulated attrs, statics, and HOCs:

const BaseButton = attrs({ name: 'Button', component: Element })
  .attrs({ tag: 'button', alignX: 'center' })

const LinkButton = BaseButton.config({ component: AnchorElement })
// Renders AnchorElement with the same attrs chain

displayName Derivation

The component name is derived in priority order:

  1. options.name (explicit name from attrs() or .config())
  2. options.component.displayName (React standard)
  3. options.component.name (function/class name)

Empty Config

.config({}) returns a new component instance with identical behavior — useful for ensuring immutability.

.compose(hocMap)

Attach named Higher-Order Components. HOCs wrap the component in reverse declaration order.

.compose({
  [hocName: string]: GenericHoc | null | undefined | false,
}): AttrsComponent

Where GenericHoc = (component: ElementType) => ElementType.

Adding HOCs

const Button = attrs({ name: 'Button', component: Element })
  .compose({
    withTheme: (Component) => (props) => (
      <ThemeProvider><Component {...props} /></ThemeProvider>
    ),
    withTracking: trackingHoc,
  })

Removing HOCs

Set a HOC to null, undefined, or false to remove it:

const Untracked = Button.compose({ withTracking: false })
// withTracking removed, withTheme preserved
const Minimal = Button.compose({ withTracking: null, withTheme: undefined })
// Both removed

Overriding HOCs

Keys identify HOCs — later .compose() calls can replace earlier HOCs by key:

const Base = attrs({ name: 'Base', component: Element })
  .compose({ wrapper: withDivWrapper })

const Override = Base.compose({ wrapper: withSpanWrapper })
// 'wrapper' HOC replaced with withSpanWrapper

Application Order

HOCs are applied in reverse declaration order. The first-declared HOC becomes the outermost wrapper:

// Declaration: { a: hocA, b: hocB, c: hocC }
// Application: hocA(hocB(hocC(EnhancedComponent)))
// Outermost wrapper: hocA (runs first on render)
// Innermost wrapper: hocC (runs last, closest to component)

The built-in attrsHoc (resolves attrs/priorityAttrs) is always outermost — it runs before any user-composed HOCs.

Multiple .compose() Calls

const Button = attrs({ name: 'Button', component: Element })
  .compose({ withOuter: outerHoc })
  .compose({ withInner: innerHoc })
// Applied as: attrsHoc → withOuter → withInner → EnhancedComponent

.statics(metadata)

Attach metadata accessible via the .meta property.

.statics({
  [key: string]: any,
}): AttrsComponent

Usage

const Button = attrs({ name: 'Button', component: Element })
  .statics({
    category: 'action',
    sizes: ['sm', 'md', 'lg'],
    variants: ['primary', 'secondary', 'danger'],
  })

Button.meta.category   // 'action'
Button.meta.sizes      // ['sm', 'md', 'lg']

Chaining

Statics merge across chains (later values override):

const Enhanced = Button.statics({ variants: ['primary', 'ghost'] })

Enhanced.meta.category   // 'action' (inherited)
Enhanced.meta.variants   // ['primary', 'ghost'] (overridden)

Immutability

Each .statics() call creates a new component with its own meta object:

const A = attrs({ name: 'A', component: Element }).statics({ x: 1 })
const B = A.statics({ y: 2 })

A.meta  // { x: 1 }
B.meta  // { x: 1, y: 2 }

.getDefaultAttrs(props)

Compute the merged default props for given input props. This is a non-mutating query method — it doesn't render anything.

.getDefaultAttrs(props: Record<string, any>): Record<string, any>

Usage

const Button = attrs({ name: 'Button', component: Element })
  .attrs(() => ({ tag: 'button' }))
  .attrs((props) => ({
    className: props.disabled ? 'disabled' : 'enabled',
  }))

Button.getDefaultAttrs({ disabled: true })
// { tag: 'button', className: 'disabled' }

Button.getDefaultAttrs({})
// { tag: 'button', className: 'enabled' }

Behavior

  • Executes all normal attrs callbacks in chain order with the provided props
  • Merges results left-to-right (later callbacks override earlier ones)
  • Does NOT include priority attrs (only resolves the normal .attrs() chain)
  • Does NOT apply prop filtering
  • Does NOT include the provided props in the result — only returns what attrs callbacks produce
  • Returns {} if no attrs are defined

Component Properties

Each AttrsComponent exposes:

PropertyTypeDescription
IS_ATTRStrueStatic marker for runtime identification via isAttrsComponent()
displayNamestringComponent name for React DevTools
metaRecord<string, any>User-defined statics from .statics() chains
$$originTypesType onlyOriginal component props type (not accessible at runtime)
$$extendedTypesType onlyExtended props from .attrs() calls (not accessible at runtime)
$$typesType onlyAll combined props: MergeTypes<[OA, EA]> (not accessible at runtime)

Ref Forwarding

Refs are forwarded through the entire HOC chain:

const Button = attrs({ name: 'Button', component: Element })
  .compose({ withFeature: someHoc })

const ref = useRef(null)
<Button ref={ref}>Click</Button>
// ref.current points to the DOM node through all HOC layers

Ref Merging

The system manages two ref sources:

  • $attrsRef — The consumer's ref (captured by attrsHoc)
  • ref — Intermediate HOC refs (from compose wrappers that use forwardRef)

Both are merged via useImperativeHandle into a single internal ref inside the EnhancedComponent, ensuring all ref sources point to the same DOM node.

API Reference

ConfigAttrs

Prop

Type

AttrsCb

Prop

Type

ComposeParam

Prop

Type

On this page