@diskette/composed-props
v0.15.0
Published
A TypeScript-first React hooks library that enables component libraries to provide render props functionality with type inference and runtime validation.
Readme
@diskette/composed-props
A TypeScript-first React hooks library that enables component libraries to provide render props functionality with type inference and runtime validation.
📦 Installation
npm install @diskette/composed-propsyarn add @diskette/composed-propspnpm add @diskette/composed-propsKey Features
- 🏭 Hook Factory Pattern: Create customized
useRenderPropshooks tailored to your component's needs - 🔒 Type Safety: Sophisticated TypeScript inference with runtime validation
- 🎯 Component Library Focus: Built specifically for library authors to expose render props APIs
- 🌟 Multi-State Support: Different props can respond to different state types
- 🔧 Flexible: Support for static values, dynamic functions, defaults, and transforms
🧩 Core Concepts
ComposableProp<T, V>
The ComposableProp<T, V> type represents a prop that can be either:
- A static value of type
V - A function
(state: T) => Vthat computes the value from component state
import type { ComposableProp } from '@diskette/composed-props'
interface ButtonState {
isHovered: boolean
isPressed: boolean
}
interface ButtonProps {
// Must explicitly use ComposableProp for each prop
backgroundColor: ComposableProp<ButtonState, string>
opacity: ComposableProp<ButtonState, number>
// Non-composable props remain normal
onClick?: () => void
}
// Static usage
<Button backgroundColor="#007bff" opacity={1} />
// Dynamic usage
<Button
backgroundColor={({ isHovered, isPressed }) =>
isPressed ? '#0056b3' : isHovered ? '#0069d9' : '#007bff'
}
opacity={({ isHovered }) => isHovered ? 0.8 : 1}
/>📚 API Reference
createUseRenderProps<Config>(config)
Creates a type-safe useRenderProps hook based on a configuration schema. This is the primary tool for component library authors.
Example: Creating a Custom Hook
import { createUseRenderProps } from '@diskette/composed-props'
import type { ComposableProp } from '@diskette/composed-props'
// Define your component's state shapes
interface TabsState {
activeTab: string
hoveredTab: string | null
}
interface ThemeState {
theme: 'light' | 'dark'
}
// Props interface using ComposableProp explicitly
interface TabsProps {
className?: ComposableProp<TabsState, string>
style?: ComposableProp<ThemeState, CSSProperties> // Different state type!
'data-active': ComposableProp<TabsState, boolean> // Required prop
children?: ComposableProp<TabsState, ReactNode>
// Non-composable props
onTabChange?: (tab: string) => void
}
// Create the hook factory with validation
const useTabsRenderProps = createUseRenderProps({
className: { type: 'string' },
style: { type: (value): value is CSSProperties => typeof value === 'object' },
'data-active': { type: 'boolean', required: true },
children: { type: (value): value is ReactNode => true },
})
// Component library implementation
function Tabs(props: TabsProps) {
const [activeTab, setActiveTab] = useState('tab1')
const [hoveredTab, setHoveredTab] = useState<string | null>(null)
const [theme, setTheme] = useState<'light' | 'dark'>('light')
const tabsState: TabsState = { activeTab, hoveredTab }
const themeState: ThemeState = { theme }
const { composed, rest } = useTabsRenderProps(props)
// Option 1: Use individual functions
const className = composed.className?.(tabsState)
const style = composed.style?.(themeState)
return (
<div
className={className}
style={style}
data-active={composed['data-active']?.(tabsState)}
{...rest}
>
{composed.children?.(tabsState)}
</div>
)
// Option 2: Use props helper with multi-state support
// const resolvedProps = composed.props({
// className: tabsState, // TabsState for className
// style: themeState, // ThemeState for style (different state!)
// 'data-active': tabsState, // TabsState for data-active
// children: tabsState // TabsState for children
// })
//
// return <div {...resolvedProps} /> // rest is already included!
}Individual Functions vs Props Helper
const { composed, rest } = useAccordionRenderProps(props, {
// Options for defaults and transforms
duration: {
default: ({ isExpanded }) => (isExpanded ? 300 : 200),
},
className: {
transform: (className, { isAnimating }) =>
isAnimating ? `${className} accordion--animating` : className,
},
})
// Approach 1: Individual functions (fine-grained control)
const className = composed.className?.(accordionState)
const style = composed.style?.(themeState)
const duration = composed.duration?.(accordionState)
return (
<div className={className} style={style} data-duration={duration} {...rest}>
{composed.children?.(accordionState)}
</div>
)
// Approach 2: Props helper
const resolvedProps = composed.props({
className: accordionState,
style: themeState, // Different state type for style!
duration: accordionState,
children: accordionState,
})
return <div {...resolvedProps} /> // rest already included