@pyreon/attrs — API Reference
Generated from
attrs'ssrc/manifest.ts— the same source that powersllms.txtand MCPget_api. Do not edit this page by hand; edit the manifest. For the conceptual guide, see attrs.
Chainable HOC factory for Pyreon components. attrs({ name, component }) wraps a component in an immutable builder that accumulates default props (.attrs()), reconfigures the base (.config()), composes named HOCs (.compose()), and attaches static metadata (.statics()). Every chain method returns a NEW component — the original is never mutated — and .attrs<P>() generics accumulate into the component's prop type. It is the chaining foundation @pyreon/rocketstyle builds on; use it directly for default-prop composition without the dimension-styling layer.
Features
attrs({ name, component }) factory — immutable chainable builder, every method returns a new component
.attrs(object | callback, { priority?, filter? }) — stacking default props; callbacks receive the current resolved props
Merge precedence priorityAttrs < attrs < explicit props — call-site props always win; undefined values never shadow defaults
.config({ name?, component?, DEBUG? }) — rename / base swap / dev logging; chains are PRESERVED across a swap
.compose({ name: hoc }) — named HOC record; falsy value removes a previously composed HOC
.statics(meta) — metadata on Component.meta, merged across calls
TypeScript accumulation — .attrs<P>() widens the prop type; $types / $originTypes / $extendedTypes expose the shapes
Descriptor-safe prop forwarding — reactive getter props survive the HOC chain (mergeProps, not spread)
Complete example
A full, end-to-end usage of the package:
import attrs, { isAttrsComponent } from '@pyreon/attrs'
import { Element } from '@pyreon/elements'
// Factory takes { name, component } — BOTH required (dev mode throws)
const Button = attrs({ name: 'Button', component: Element })
// Object form — static default props
.attrs({ tag: 'button', alignX: 'center', alignY: 'center' })
// Callback form — receives the current resolved props; the generic
// accumulates into the component's prop type
.attrs<{ primary?: boolean }>(({ primary }) => ({
backgroundColor: primary ? 'blue' : 'gray',
}))
// Defaults apply; explicit call-site props always win
<Button label="Click me" />
<Button tag="a" href="/x" label="Link" /> // tag: 'a' overrides the default
// Strip internal control props before they reach the base / DOM
const Card = attrs({ name: 'Card', component: Element })
.attrs(({ elevated }) => ({ shadow: elevated ? 'lg' : 'none' }), { filter: ['elevated'] })
// Swap the base — attrs' .config PRESERVES the accumulated chains
// (unlike @pyreon/rocketstyle, which resets them on a component swap)
const Anchor = Button.config({ component: 'a', name: 'Anchor' })
// Named HOC composition — falsy value removes a previously composed HOC
const Tracked = Button.compose({ withTracking: (C) => (props) => C(props) })
const Untracked = Tracked.compose({ withTracking: null })
// Static metadata lands on .meta
const Tagged = Button.statics({ category: 'action' })
Tagged.meta.category // 'action'
// Introspection
Button.getDefaultAttrs({}) // resolved default props
isAttrsComponent(Button) // true (IS_ATTRS marker)Exports
| Symbol | Kind | Summary |
|---|---|---|
attrs | function | Factory entry (default + named export). |
.attrs() | function | Add default props. |
.config() | function | Reconfigure the builder: rename (name → new displayName), swap the underlying base component (component), or toggl |
.compose() | function | Attach named higher-order components. |
.statics() | function | Attach arbitrary metadata, readable on the component's .meta object. |
.getDefaultAttrs() | function | Resolve the accumulated .attrs() chain for a given props bag — every callback in the chain runs against props and th |
isAttrsComponent | function | Runtime type guard — true when a value was created by attrs() (checks the own IS_ATTRS marker). |
API
attrs function
<C extends ElementType>({ name, component }: { name: string; component: C }) => AttrsComponentFactory entry (default + named export). Wraps component in a Pyreon ComponentFn enhanced with the chain methods (.attrs() / .config() / .compose() / .statics() / .getDefaultAttrs()). name becomes the displayName and, in dev builds, a data-attrs attribute on the rendered output for debugging. Both fields are required — dev mode throws a descriptive error on a missing one. Non-React statics from the base are hoisted onto the wrapper so Base.someStatic survives the chain; the component carries the IS_ATTRS: true marker.
Example
import attrs from '@pyreon/attrs'
import { Element } from '@pyreon/elements'
const Button = attrs({ name: 'Button', component: Element })
.attrs({ tag: 'button', alignX: 'center' })
.attrs<{ primary?: boolean }>(({ primary }) => ({
backgroundColor: primary ? 'blue' : 'gray',
}))
<Button label="Click me" />
<Button tag="a" href="/x" /> // explicit props override the defaultsCommon mistakes
Calling the factory with a bare component —
attrs(Element)is wrong; the argument is the object{ name, component }and both keys are required (dev mode throws)Expecting chain calls to mutate — every method returns a NEW component;
Button.attrs({...})without assigning the return value changes nothingExpecting deep merges — defaults are shallow-merged; an object-valued prop (
style={{ color: "red" }}) from the call site REPLACES the default object, it does not combine with itStacking very deep
.attrs<P>()generic chains — TypeScript's recursive conditional-type inference caps at roughly 24-50 levels depending on the host; past that, narrow the generics or split the componentForwarding props onward with a plain spread inside a composed HOC —
{ ...props }fires reactive getter props once and collapses them to static values; pass by reference or merge withmergePropsfrom@pyreon/coreRelying on the
data-attrsdebug attribute in production — it is dev-only (process.env.NODE_ENV !== "production") and tree-shaken from production builds
See also: isAttrsComponent · @pyreon/rocketstyle · @pyreon/core
.attrs() function
<P>(attrs: object | ((props) => object), opts?: { priority?: boolean; filter?: string[] }) => AttrsComponentAdd default props. Object form for static defaults; callback form receives the CURRENT resolved props (priority attrs + explicit props) and returns a partial. Calls stack — later .attrs() calls override earlier ones for the same key. Render-time merge precedence is priorityAttrs < attrs < explicit call-site props (last wins); explicit undefined values are stripped first so they never shadow a default. { priority: true } routes the entry to the priority chain — resolved FIRST and visible as input to later .attrs() callbacks, but LOWEST precedence in the final merge. { filter: [...] } strips those prop names before they reach the base component; filter lists accumulate across the chain. The <P> generic widens the component's prop type.
Example
const Input = attrs({ name: 'Input', component: Element })
// Callback form — reads the resolved props
.attrs<{ error?: boolean }>((props) => ({
'aria-invalid': props.error ? 'true' : undefined,
}))
// Keep the control prop off the DOM
.attrs({}, { filter: ['error'] })
// Later calls override earlier ones
const Base = attrs({ name: 'Base', component: Element }).attrs({ size: 'md' })
const Small = Base.attrs({ size: 'sm' }) // → size: 'sm'Common mistakes
Assuming
{ priority: true }means highest precedence — despite the name, priority attrs are resolved EARLY (so normal.attrs()callbacks can read them as input) but sit at the LOWEST precedence in the final merge:priorityAttrs < attrs < explicit propsExpecting an explicit
undefinedat the call site to defeat a default —undefined-valued props are stripped before merging, so the.attrs()default still appliesExpecting the callback to re-run reactively —
.attrs()callbacks run once per mount during the HOC's prop resolution; reactive getter props flowing THROUGH the merge stay live for downstream JSX, but a value the callback READS is a one-shot snapshotForgetting
filteraccumulates — every name listed in any.attrs(..., { filter })call along the chain is stripped; a later call cannot un-filter an earlier nameConfusing this chain method with rocketstyle's
.attrs()— the rocketstyle variant passes extra callback arguments(props, theme, helpers); the plain attrs callback receives only the resolved props
See also: .config() · .getDefaultAttrs()
.config() function
(opts: { name?: string; component?: ElementType; DEBUG?: boolean }) => AttrsComponentReconfigure the builder: rename (name → new displayName), swap the underlying base component (component), or toggle dev debugging (DEBUG). Returns a new component; the original keeps its own name/base. Unlike @pyreon/rocketstyle, swapping component at this layer PRESERVES the accumulated .attrs() / priorityAttrs / filter / .compose() / .statics() chains and re-applies them to the new base — reconciling the new base's prop shape is the caller's responsibility.
Example
const Button = attrs({ name: 'Button', component: Element }).attrs({ tag: 'button' })
const Renamed = Button.config({ name: 'PrimaryButton' })
Renamed.displayName // 'PrimaryButton'
Button.displayName // 'Button' — original untouched
// Base swap — the .attrs() chain still applies to the new base
const Anchor = Button.config({ component: 'a', name: 'Anchor' })Common mistakes
Expecting the rocketstyle chain-reset behavior —
@pyreon/attrs'.config({ component })KEEPS the accumulated chains across a base swap (test-locked); only@pyreon/rocketstyle's.config()resets prop-shape-coupled chains. If the new base has a different prop shape, stale defaults can leak invalid props — audit them yourselfPassing dimension or theme options — this
.config()accepts onlyname/component/DEBUG; dimensions/provider/consumer/inversed are@pyreon/rocketstyle.config()surfaceReading
displayNameoff the original after renaming —.config()is immutable; the rename lands on the RETURNED component
See also: attrs · .attrs() · @pyreon/rocketstyle
.compose() function
(hocs: Record<string, ((c: ComponentFn) => ComponentFn) | null | false>) => AttrsComponentAttach named higher-order components. The argument is a RECORD of { name: hoc } — the name is the removal handle: a later .compose({ name: null }) (or undefined / false) removes that HOC from the chain; only function values are kept. Application order: the record's values are reversed so the LAST-defined HOC wraps innermost, and the built-in attrs HOC (which resolves the .attrs() chain) is always the outermost wrapper — default props are computed before any user HOC runs.
Example
const withTheme = (Component) => (props) => Component(props)
const withTracking = (Component) => (props) => Component(props)
const Enhanced = attrs({ name: 'Button', component: Element })
.compose({ withTheme, withTracking })
// Remove one by its name
const NoTracking = Enhanced.compose({ withTracking: null })Common mistakes
Passing an array of HOCs —
.compose()takes a named record; the names are what make falsy-removal possibleA composed HOC that value-copies props (
const next = { ...props }) — fires reactive getter props at setup and collapses them to static values; copy descriptors (mergeProps/splitPropsfrom@pyreon/core) or pass by referenceAssuming record order equals wrap order outside-in — values are REVERSED before composition, so the last-defined HOC runs closest to the component; the attrs HOC always stays outermost regardless
See also: attrs · .config()
.statics() function
(meta: Record<string, unknown>) => AttrsComponentAttach arbitrary metadata, readable on the component's .meta object. Successive .statics() calls merge (later keys win). Used by systems that need post-construction component introspection — e.g. @pyreon/document-primitives reads _documentType this way.
Example
const Btn = attrs({ name: 'Btn', component: Element }).statics({
category: 'action',
sizes: ['sm', 'md', 'lg'],
})
Btn.meta.category // 'action'
Btn.meta.sizes // ['sm', 'md', 'lg']Common mistakes
Reading statics directly off the component — in
@pyreon/attrsthey land onComponent.meta, NOT on the component object itself (rocketstyle's.statics()additionally assigns onto the component; this layer does not)Using
.statics()for per-instance data — statics are definition-level metadata shared by every instance
See also: .compose() · isAttrsComponent
.getDefaultAttrs() function
(props: TObj) => TObjResolve the accumulated .attrs() chain for a given props bag — every callback in the chain runs against props and the results merge left-to-right (later calls win). Useful for introspection and testing the resolved defaults without mounting.
Example
const Button = attrs({ name: 'Button', component: Element })
.attrs({ tag: 'button', alignX: 'center' })
.attrs<{ primary?: boolean }>(({ primary }) => ({ kind: primary ? 'primary' : 'plain' }))
Button.getDefaultAttrs({}) // { tag: 'button', alignX: 'center', kind: 'plain' }
Button.getDefaultAttrs({ primary: true }) // { ..., kind: 'primary' }Common mistakes
Expecting priority attrs in the result —
getDefaultAttrsresolves only the normal.attrs()chain, notpriorityAttrsCalling with no argument when callbacks destructure props — pass at least
{}so({ primary }) => ...callbacks don't destructureundefined
See also: .attrs()
isAttrsComponent function
<T>(component: T) => booleanRuntime type guard — true when a value was created by attrs() (checks the own IS_ATTRS marker). Use it to discriminate attrs-wrapped components from plain functions; typeof value === "function" cannot tell them apart because an attrs component IS callable.
Example
import { isAttrsComponent } from '@pyreon/attrs'
isAttrsComponent(Button) // true
isAttrsComponent('div') // false
isAttrsComponent(() => null) // false — plain functions lack the markerCommon mistakes
Discriminating with
typeof value === "function"— attrs components are callable, so use the marker guard insteadTesting a rocketstyle component — rocketstyle components carry
IS_ROCKETSTYLE, notIS_ATTRS; useisRocketComponentfrom@pyreon/rocketstylefor those
See also: attrs · @pyreon/rocketstyle
Package-level notes
Note: 'priority' does NOT mean 'highest precedence' — priorityAttrs resolve FIRST (so later .attrs() callbacks can read them as input) but sit at the LOWEST precedence in the final merge: priorityAttrs < attrs < explicit props. @pyreon/rocketstyle uses the slot to seed structural base props the dimension layer can still override.
Chains survive base swaps:
.config({ component })at this layer PRESERVES the accumulated chains and re-applies them to the new base — the chain-reset-on-swap behavior belongs to@pyreon/rocketstyle, one layer up. Reconciling the new base's prop shape is on the caller.
Type accumulation: Each
.attrs<P>()generic accumulates into the component's prop type; the type-only$types(origin + extended),$originTypes, and$extendedTypesproperties expose the accumulated shapes, andExtractProps<typeof C>from@pyreon/corerecovers the union for HOC forwarding.
Descriptor-safe forwarding: The attrs HOC merges consumer props via
mergeProps/removeUndefinedPropsfrom@pyreon/core(descriptor copies), so compiler-emitted reactive getter props survive the chain. Any HOC you.compose()must do the same — a plain{ ...props }spread collapses reactive props to static values.