@ffsm/compositor
@ffsm/compositor
is a collection of utility components that make React component composition
more declarative and maintainable. These components solve common UI patterns in a consistent way,
reducing boilerplate and making code more readable.
Installation
# Using npm
npm install @ffsm/compositor
# Using yarn
yarn add @ffsm/compositor
# Using pnpm
pnpm add @ffsm/compositor
Features Overview
- Declarative Composition: Replace imperative logic with declarative components
- Prop Injection: Easily manage and propagate props through component hierarchies
- Conditional Rendering: Simplify conditional UI patterns
- Type Safety: Full TypeScript support with proper generic types
- Small Footprint: Lightweight implementation with minimal dependencies
- Customizable: Flexible API supporting various composition patterns
When to Use
The compositor library is particularly useful when:
- Building component libraries with consistent composition patterns
- Managing complex conditional rendering logic
- Creating reusable layout components
- Implementing slot-based component architectures
- Reducing boilerplate in React applications
Component Overview
Component | Purpose | When to Use |
---|---|---|
AsInstance |
Prop merging for a single element | When you need to extend an element with additional props |
AsArray |
Batch operations on multiple children | When working with collections of elements that need shared props or transformations |
AsNode |
Conditional rendering (if) | When you need to conditionally render content based on a single condition |
AsSlot |
Content projection into wrappers | When implementing component composition with slots or insertion points |
Condition |
Conditional rendering (if/else) | When you need to choose between two rendering paths |
Empty |
Empty state handling | When working with potentially empty data or content |
Components
AsInstance Component
AsInstance
is a utility component that helps with prop composition by merging additional props with a React element's existing props.
Features
- Prop Merging: Combines specified props with a React element's existing props
- Safe Handling: Gracefully handles non-element children
- Simple API: Straightforward usage pattern with minimal boilerplate
- Type Safety: Full TypeScript support with proper type definitions
Basic Usage
import { AsInstance } from '@ffsmio/compositor';
function App() {
return (
<AsInstance className="enhanced" data-testid="submit-btn">
<button onClick={handleClick}>Submit</button>
</AsInstance>
);
}
This renders the button with its original onClick
handler plus the new className
and data-testid
props.
Prop Merging Behavior
When both the child element and the AsInstance
wrapper specify the same prop, the wrapper's prop takes precedence:
<AsInstance className="override-class">
<div className="original-class">Content</div>
</AsInstance>
This renders: <div className="override-class">Content</div>
Working with Non-Element Children
If you pass a non-element child (like plain text, numbers, null, or undefined), AsInstance
returns it unchanged:
<AsInstance className="will-be-ignored">Just some plain text</AsInstance>
This renders: Just some plain text
Use Cases
- Applying theme props: Add theme-related props to components
- Adding accessibility attributes: Enhance components with aria attributes
- Component composition: Create higher-order components that add behavior
- Dynamic props: Add conditional props based on application state
API Reference
Props
Prop | Type | Description |
---|---|---|
children |
ReactNode | Child element to receive merged props |
...rest |
any | Additional props to merge with the child element |
Implementation Notes
- Uses React's
cloneElement
under the hood for prop merging - Performs proper type checking with
isValidElement
before attempting to clone - Preserves the child's original component identity and ref
AsArray Component
AsArray
is a utility component that makes working with collections of React children more powerful by providing filtering and transformation capabilities.
Features
- Prop Inheritance: Pass props to all children at once
- Filtering: Include only specific children using a filter function
- Transformation: Transform children with a mapping function
- Key Management: Automatically handles React's key requirements
Basic Usage
import { AsArray } from '@ffsmio/compositor';
function App() {
return (
<AsArray className="shared-class" data-testid="group">
<button>First Button</button>
<button>Second Button</button>
<button>Third Button</button>
</AsArray>
);
}
This renders three buttons, each with the className="shared-class"
and data-testid="group"
props.
Filtering Children
Use the filter
prop to selectively include children:
<AsArray
filter={(child, index) => {
// Only include even-indexed children
return index % 2 === 0;
}}
className="even-only"
>
<div>Item 0</div>
<div>Item 1</div>
<div>Item 2</div>
<div>Item 3</div>
</AsArray>
Transforming Children
Use the map
prop to transform children:
<AsArray
map={(child, index) => {
// Add index to each child's content
if (React.isValidElement(child)) {
return React.cloneElement(
child,
child.props,
`${child.props.children} (${index})`
);
}
return child;
}}
>
<li>Apple</li>
<li>Banana</li>
<li>Cherry</li>
</AsArray>
Renders:
- Apple (0)
- Banana (1)
- Cherry (2)
API Reference
Props
Prop | Type | Description |
---|---|---|
children |
ReactNode | Child elements to process |
filter |
(child: ReactNode, index: number) => boolean | Optional function to filter children |
map |
(child: ReactNode, index: number) => ReactNode | Optional function to transform children |
...rest |
any | Additional props passed to all children |
Notes
- All children are rendered inside an
AsInstance
component, which handles proper prop merging - The component internally uses React's
Children.toArray()
for stable keys and array operations - When filtering, children are excluded completely rather than rendered conditionally
AsNode Component
AsNode
is a declarative conditional rendering component that simplifies the common pattern of rendering content only when a condition is met.
Features
- Simplified Conditional Rendering: Replaces ternary expressions and
&&
patterns - Declarative API: Makes conditional rendering more readable
- Function Conditions: Supports functions and async functions as conditions
- Falsy Value Handling: Optional strict falsy checking for empty strings, zero, etc.
- Prop Forwarding: Passes additional props to rendered children
Basic Usage
import { AsNode } from '@ffsmio/compositor';
function UserSection({ user }) {
return (
<AsNode of={user}>
<div className="user-info">
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
</AsNode>
);
}
This renders the user info div only when user
exists (is not undefined
or false
).
Using Function Conditions
You can use a function as the condition, which is useful for dynamic evaluations:
<AsNode of={(props) => userService.hasPermission('admin')}>
<AdminPanel />
</AsNode>
The function receives all props passed to AsNode, allowing for contextual conditions.
Async Conditions
AsNode also supports async functions for conditions that need to be resolved:
<AsNode of={async () => await checkUserSubscription()}>
<PremiumContent />
</AsNode>
Comparing with Traditional Conditional Rendering
Traditional approach:
function UserSection({ user }) {
return user ? (
<div className="user-info">
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
) : null;
}
With AsNode
:
function UserSection({ user }) {
return (
<AsNode of={user}>
<div className="user-info">
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
</AsNode>
);
}
Enhanced Falsy Checking
By default, AsNode
only treats undefined
and false
as falsy. To extend this to all JavaScript falsy values (empty strings, 0, NaN, null), use the falsy
prop:
<AsNode of={searchResults.length} falsy>
<SearchResultsList results={searchResults} />
</AsNode>
This will only render the list when there are actual results.
Passing Props to Children
AsNode
uses AsInstance
internally, so any additional props will be passed to the children:
<AsNode of={isAdmin} className="admin-panel" data-testid="admin-section">
<AdminControls />
</AsNode>
API Reference
Props
Prop | Type | Default | Description |
---|---|---|---|
children |
ReactNode | required | Content to conditionally render |
of |
unknown | undefined | The condition that determines if children render. Maybe using as a function or promise function |
falsy |
boolean | false | When true, any falsy value prevents rendering |
...rest |
any | - | Additional props passed to children through AsInstance |
Use Cases
- Conditional rendering based on user permissions
- Showing components only when data is available
- Feature flags and toggles
- Simplifying complex conditional rendering logic
- Dynamic conditions that depend on runtime state or API calls
AsSlot Component
AsSlot
implements a slot-based composition pattern for React, allowing children to be rendered within a specified outlet component or through a render function.
Features
- Slot-Based Composition: Inject content into wrapper components
- Flexible API: Use either component outlets or render functions
- Prop Forwarding: Pass props to both the outlet and the content
- Ref Handling: Properly forwards refs between components
Basic Usage
import { AsSlot } from '@ffsmio/compositor';
import { Card } from './components';
function UserProfile({ user }) {
return (
<AsSlot outlet={<Card />}>
<h2>{user.name}</h2>
<p>{user.bio}</p>
</AsSlot>
);
}
This renders the user profile content inside the Card
component.
Using Outlet Props
You can pass specific props to the outlet component using outletProps
:
<AsSlot
outlet={<Panel />}
outletProps={{
title: 'Settings',
collapsible: true,
defaultExpanded: true,
}}
>
<SettingsForm />
</AsSlot>
Render Function Pattern
For more dynamic scenarios, use a render function as the outlet:
<AsSlot
outlet={(props) => (
<Modal isOpen={isModalOpen} onClose={handleClose} {...props} />
)}
className="modal-content"
>
<h2>Confirm Deletion</h2>
<p>This action cannot be undone.</p>
<div className="button-group">
<button onClick={handleConfirm}>Delete</button>
<button onClick={handleCancel}>Cancel</button>
</div>
</AsSlot>
Passing Props to Children
Additional props are passed to the children via AsInstance
:
<AsSlot outlet={<Card />} className="highlighted" data-testid="user-card">
<UserProfile />
</AsSlot>
Comparison with Traditional Composition
Traditional approach:
<Card>
<div className="highlighted" data-testid="user-card">
<UserProfile />
</div>
</Card>
With AsSlot
:
<AsSlot outlet={<Card />} className="highlighted" data-testid="user-card">
<UserProfile />
</AsSlot>
API Reference
Props
Prop | Type | Description |
---|---|---|
children |
ReactNode | Content to render inside the outlet |
outlet |
ReactNode | RenderFunction<Props> | Component or function to wrap children |
outletProps |
ObjectProps | Props to pass to the outlet component |
...rest |
ObjectProps | Additional props passed to children via AsInstance |
Type Definitions
type ObjectProps = Record<string, any>;
type RenderFunction<Props> = (props: Props) => ReactNode;
Use Cases
- Creating composite UI patterns like cards, panels, and dialogs
- Building component libraries with consistent wrappers
- Implementing layout components with customizable content areas
- Creating higher-order components with enhanced behavior
Condition Component
Condition
is a declarative conditional rendering component that simplifies rendering different content based on conditions, with support for fallback content.
Features
- If/Else Pattern: Renders either main content or fallback content
- Declarative API: Makes conditional rendering more readable
- Function Conditions: Supports functions and async functions for dynamic evaluation
- Falsy Value Handling: Optional strict falsy checking
- Prop Forwarding: Passes props to whichever content is rendered
Basic Usage
import { Condition } from '@ffsmio/compositor';
function ProfileSection({ user, isLoading }) {
return (
<Condition when={!isLoading && user} fallback={<LoadingSpinner />}>
<UserProfile data={user} />
</Condition>
);
}
This renders the UserProfile
when a user exists and it's not loading, or a LoadingSpinner
otherwise.
Using Function Conditions
You can use a function as the condition, which is useful for dynamic evaluations:
<Condition
when={(props) => userService.hasPermission('admin')}
fallback={<AccessDenied />}
>
<AdminPanel />
</Condition>
The function receives all props passed to Condition, allowing for contextual conditions.
Async Conditions
Condition also supports async functions for conditions that need to be resolved:
<Condition
when={async () => await checkUserSubscription()}
fallback={<SubscribePrompt />}
>
<PremiumContent />
</Condition>
Enhanced Falsy Checking
By default, Condition
only treats undefined
and false
as falsy. To extend this to all JavaScript falsy values (empty strings, 0, NaN, null), use the falsy
prop:
<Condition
when={searchResults.length}
falsy
fallback={<EmptyState message="No results found" />}
>
<SearchResults items={searchResults} />
</Condition>
Passing Props to Rendered Content
Condition
uses AsInstance
internally, so any additional props will be passed to whichever content is rendered:
<Condition
when={isAuthenticated}
fallback={<LoginPage />}
className="main-content"
data-testid="content-section"
>
<Dashboard />
</Condition>
No Fallback
If you don't provide a fallback, nothing is rendered when the condition is falsy:
<Condition when={showBanner}>
<AnnouncementBanner message={bannerText} />
</Condition>
Comparing with Traditional Conditional Rendering
Traditional approach:
function ProfileSection({ user, isLoading }) {
return !isLoading && user ? (
<UserProfile data={user} className="profile-section" />
) : (
<LoadingSpinner className="profile-section" />
);
}
With Condition
:
function ProfileSection({ user, isLoading }) {
return (
<Condition
when={!isLoading && user}
fallback={<LoadingSpinner />}
className="profile-section"
>
<UserProfile data={user} />
</Condition>
);
}
API Reference
Props
Prop | Type | Default | Description |
---|---|---|---|
children |
ReactNode | required | Content to display when condition is truthy |
when |
unknown | undefined | The condition that determines which content to show, maybe using as a function or promise function |
falsy |
boolean | false | When true, any falsy value triggers fallback |
fallback |
ReactNode | undefined | Content to display when condition is falsy |
...rest |
any | - | Props passed to whichever content is rendered |
Use Cases
- Toggling between loading states and loaded content
- Showing different UI based on user permissions or roles
- Displaying error states when operations fail
- Implementing feature flags or experimental features
- Dynamic conditions that depend on runtime state or API calls
Empty Component
Empty
is a utility component that simplifies handling empty or undefined children by rendering fallback content when needed.
Features
- Empty State Handling: Automatically detects undefined or false children
- Fallback Content: Provides alternative content when children are empty
- Falsy Value Detection: Optional strict checking for all falsy values
- Prop Forwarding: Passes props to whichever content is rendered
Basic Usage
import { Empty } from '@ffsmio/compositor';
function UserDetails({ user }) {
return (
<Empty fallback={<p>No user information available</p>}>
{user && (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
)}
</Empty>
);
}
This renders the user details when they exist, or the fallback message when user
is falsy.
Enhanced Falsy Checking
By default, Empty
only treats undefined
and false
as empty. To extend this to all JavaScript falsy values (empty strings, 0, NaN, null), use the falsy
prop:
<Empty fallback={<NoResultsView />} falsy>
{searchResults.length && <ResultsList results={searchResults} />}
</Empty>
Passing Props
Empty
uses AsInstance
internally, so any additional props will be passed to whichever content is rendered:
<Empty
fallback={<EmptyState />}
className="content-container"
data-testid="results-area"
>
{data}
</Empty>
Comparing with Traditional Patterns
Traditional approach:
function MessageDisplay({ message }) {
return message ? (
<div className="message">{message}</div>
) : (
<div className="message">No message available</div>
);
}
With Empty
:
function MessageDisplay({ message }) {
return (
<Empty fallback="No message available" className="message">
{message}
</Empty>
);
}
API Reference
Props
Prop | Type | Default | Description |
---|---|---|---|
children |
ReactNode | - | The primary content to render if not empty |
fallback |
ReactNode | undefined | Content to display when children are empty |
falsy |
boolean | false | When true, any falsy value triggers fallback |
...rest |
any | - | Props passed to whichever content is rendered |
Use Cases
- Displaying placeholders when data is not available
- Creating components with meaningful empty states
- Building more resilient UI components
- Simplifying conditional rendering in JSX
Utilities
createEvent
The createEvent
function creates custom events for the Compositor system with full support for event bubbling, propagation control, and default action prevention.
Basic Usage
import { createEvent } from '@ffsmio/compositor';
// Create a basic event
const myEvent = createEvent('button-click', { id: 'submit-button' });
// Use the event
element.dispatchEvent(myEvent);
Type Safety with Generics
// Define a custom event type
interface ClickEvent {
name: string;
value: {
x: number;
y: number;
};
preventDefault(): void;
stopPropagation(): void;
}
// Create a strongly-typed event
const clickEvent = createEvent<ClickEvent>('click', { x: 100, y: 200 });
Event Features
Created events include:
- Event naming: Associate a name with your event
- Custom payload: Attach any value to your event
- Bubbling control: Events bubble by default, configurable via constructor
- Cancellation: Events can be cancelled using
preventDefault()
- Propagation control: Stop event propagation with
stopPropagation()
- Target tracking: Both original target and current target are tracked
Event Properties and Methods
Property/Method | Description |
---|---|
name |
Event name identifier |
value |
Event payload data |
target |
Original event target |
currentTarget |
Current target in the propagation path |
preventDefault() |
Prevents the default action |
stopPropagation() |
Stops event propagation |
isDefaultPrevented() |
Checks if default action was prevented |
isPropagationStopped() |
Checks if propagation was stopped |
Advanced Usage
Composition Patterns
Components can be composed to create more complex patterns:
<Condition when={hasData} fallback={<LoadingState />}>
<AsArray filter={(item) => item.isVisible} className="data-item">
{data.map((item) => (
<DataItem key={item.id} {...item} />
))}
</AsArray>
</Condition>
Custom Hooks Integration
The components work well with custom hooks:
function useUserData() {
const [data, setData] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
// Fetch logic...
return { data, isLoading, error };
}
function UserProfile() {
const { data, isLoading, error } = useUserData();
return (
<Condition when={!isLoading} fallback={<LoadingSpinner />}>
<Condition when={!error} fallback={<ErrorMessage error={error} />}>
<UserCard user={data} />
</Condition>
</Condition>
);
}
Performance Considerations
- All components are optimized for minimal re-renders
- When using
AsArray
with large lists, consider memoizing filter and map functions - For deeply nested component trees, consider composition at appropriate levels rather than passing props through many layers
Browser Support
- Supports all modern browsers
- IE11 compatible with appropriate polyfills
- Works in both client-side and server-side rendering environments
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature
) - Commit your changes (
git commit -m 'Add some amazing feature'
) - Push to the branch (
git push origin feature/amazing-feature
) - Open a Pull Request
License
This project is licensed under the MIT License - see the LICENSE file for details.