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[]
}
): AttrsComponentObject 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[],
})| Option | Default | Description |
|---|---|---|
priority | false | When 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 attrsMerge order:
- Priority attrs callbacks called with filtered explicit props
- Normal attrs callbacks called with
{ ...priorityResult, ...filteredExplicitProps } - 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 propFilters 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| Parameter | Type | Description |
|---|---|---|
name | string | Updates displayName. If omitted, keeps existing name. |
component | ElementType | Replaces the wrapped component. Props type (OA) re-extracted from new component. |
DEBUG | boolean | Debug flag (reserved for future use). |
Rename
const Primary = Button.config({ name: 'PrimaryButton' })
// React DevTools shows "PrimaryButton"
// data-attrs="PrimaryButton" in developmentSwap 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 chaindisplayName Derivation
The component name is derived in priority order:
options.name(explicit name fromattrs()or.config())options.component.displayName(React standard)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,
}): AttrsComponentWhere 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 preservedconst Minimal = Button.compose({ withTracking: null, withTheme: undefined })
// Both removedOverriding 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 withSpanWrapperApplication 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,
}): AttrsComponentUsage
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:
| Property | Type | Description |
|---|---|---|
IS_ATTRS | true | Static marker for runtime identification via isAttrsComponent() |
displayName | string | Component name for React DevTools |
meta | Record<string, any> | User-defined statics from .statics() chains |
$$originTypes | Type only | Original component props type (not accessible at runtime) |
$$extendedTypes | Type only | Extended props from .attrs() calls (not accessible at runtime) |
$$types | Type only | All 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 layersRef Merging
The system manages two ref sources:
$attrsRef— The consumer's ref (captured byattrsHoc)ref— Intermediate HOC refs (fromcomposewrappers that useforwardRef)
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