0.4.1 • Published 7 months ago

@noema/use-composable-props v0.4.1

Weekly downloads
-
License
MIT
Repository
github
Last release
7 months ago

@noema/use-composable-props

React hooks that enable composable props patterns, allowing components to accept either static values or functions that receive render props.

✨ Features

  • šŸŽÆ Dynamic Props: Transform static props into dynamic functions that respond to component state. Choose between composable functions or pre-resolved values.
  • šŸ”§ Flexible Composition: Compose props with transform and/or fallback logic
  • šŸŽ­ TypeScript First: Intelligent type inference

šŸ“¦ Installation

npm install @noema/composed-props
yarn add @noema/composed-props
pnpm add @noema/composed-props

šŸš€ Quick Start

import React, { useState, ReactNode } from 'react'
import { useComposedProps, ComposableProp } from '@noema/composed-props'

type State = { isHovered: boolean }
type Variant = 'primary' | 'secondary'
interface ButtonProps {
	children?: ComposableProp<State, ReactNode>
	className?: ComposableProp<State & { variant: Variant }, string>
	variant: Variant
}

function DynamicButton({ children, className, ...props }: ButtonProps) {
	const [isHovered, setIsHovered] = useState(false)

	const resolved = useComposedProps({ children, className }, {
    children: { isHovered },
    className: { isHovered, variant: 'primary' }
  })

	return (
		<button
			className={resolved.className}
			onMouseEnter={() => setIsHovered(true)}
			onMouseLeave={() => setIsHovered(false)}
			{...props}
		>
			{resolved.children}
		</button>
	)
}

// Usage
;<DynamicButton
	label={({ isHovered }) => (isHovered ? 'Click me! šŸŽÆ' : 'Hover me')}
	className={({ isHovered, variant }) => `btn ${isHovered && variant === 'primary ? 'btn-hover' : 'btn-normal'}`}
	variant="primary"
/>

šŸ“š API Reference

useComposedProps(props, renderProps, options?)

Returns all props already resolved/composed with the provided render props.

const resolved = useComposedProps(props, state, options)

Parameters:

  • props - Object containing your component props (some may be functions)
  • state - State/context object passed to composable prop functions
  • options? - Optional configuration for individual props

Returns: Object with all props resolved to their final values

useComposableProps(props, options?)

Returns functions that can be called with render props to resolve values on-demand.

const composed = useComposableProps(props, options)

Parameters:

  • props - Object containing your component props
  • options? - Optional configuration for individual props

Returns: Object with functions for each prop that accept render props

ComposeOptions<T, U, V>

Configuration object for customizing prop composition behavior:

interface ComposeOptions<T, U, V> {
	fallback?: (props: U) => V // Used when prop is undefined
	transform?: (value: V, props: U) => V // Transform final value
}

šŸŽÆ Examples

With Options

import { useComposableProps, ComposableProp } from '@noema/use-composable-props'

interface TooltipProps {
	content: ComposableProp<{ isOpen: boolean }, string>
	position: ComposableProp<{ isOpen: boolean }, 'top' | 'bottom'>
}

function Tooltip(props: TooltipProps) {
	const composed = useComposableProps(props, {
		content: {
			fallback: () => 'Default tooltip',
			transform: (content, state) => (state.isOpen ? content : ''),
		},
		position: {
			render: (staticPosition, state) =>
				state.isOpen ? staticPosition : 'top',
		},
	})

	const [isOpen, setIsOpen] = useState(false)
	const state = { isOpen }

	return (
		<div className={`tooltip tooltip-${composed.position(state)}`}>
			{composed.content(state)}
		</div>
	)
}

Complex State Management

import { useComposableProps, ComposableProp } from '@noema/use-composable-props'

interface DataTableProps {
  columns: ComposableProp<TableState, ColumnDef[]>
  data: ComposableProp<TableState, any[]>
  loading?: ComposableProp<TableState, boolean>
}

interface TableState {
  sortBy: string
  sortOrder: 'asc' | 'desc'
  filters: Record<string, any>
  pagination: { page: number; size: number }
}

function DataTable(props: DataTableProps) {
  const composed = useComposableProps(props)

  const [tableState, setTableState] = useState<TableState>({
    sortBy: '',
    sortOrder: 'asc',
    filters: {},
    pagination: { page: 1, size: 10 }
  })

  return (
    <div>
      {composed.loading?.(tableState) && <LoadingSpinner />}
      <table>
        <thead>
          {composed.columns(tableState).map(col => (
            <th key={col.key}>{col.title}</th>
          ))}
        </thead>
        <tbody>
          {composed.data(tableState).map((row, i) => (
            <tr key={i}>{/* render row */}</tr>
          ))}
        </tbody>
      </table>
    </div>
  )
}

// Usage
<DataTable
  columns={[
    { key: 'name', title: 'Name' },
    { key: 'email', title: 'Email' }
  ]}
  data={(state) =>
    users
      .filter(user => /* apply state.filters */)
      .sort((a, b) => /* apply state.sortBy and state.sortOrder */)
      .slice(/* apply state.pagination */)
  }
  loading={(state) => state.filters.search?.length > 0 && isSearching}
/>

šŸŽ­ TypeScript Support

The library is built with TypeScript and provides excellent type inference:

interface MyProps {
	text: ComposableProp<AppState, string>
	count: ComposableProp<AppState, number>
}

// TypeScript automatically infers the correct types
const resolved = useComposedProps(props, state)
// resolved.text: string
// resolved.count: string

šŸ¤ When to Use Each Hook

useComposedProps

āœ… Use when:

  • You want convenience and clean syntax
  • Render props are stable and don't change frequently
  • You prefer declarative style
  • You want all props resolved at once

useComposableProps

āœ… Use when:

  • You need fine-grained control over when props are resolved
  • Render props change frequently during render
  • You want to optimize performance by avoiding unnecessary resolutions
  • You need to call the same composable prop with different states
0.4.1

7 months ago

0.3.1

7 months ago

0.3.0

7 months ago

0.2.0

7 months ago

0.1.0

7 months ago