@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.
# Using npm
npm install @ffsm/compositor
# Using yarn
yarn add @ffsm/compositor
# Using pnpm
pnpm add @ffsm/compositor
- 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
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 | 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 |
AsInstance
is a utility component that helps with prop composition by merging additional props with a React element's existing props.
- 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
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.
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>
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
- 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
Prop | Type | Description |
---|---|---|
children |
ReactNode | Child element to receive merged props |
...rest |
any | Additional props to merge with the child element |
- 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
is a utility component that makes working with collections of React children more powerful by providing filtering and transformation capabilities.
- 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
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.
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>
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)
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 |
- 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
is a declarative conditional rendering component that simplifies the common pattern of rendering content only when a condition is met.
-
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
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>
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>
);
}
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.
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>
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
|
- 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
implements a slot-based composition pattern for React, allowing children to be rendered within a specified outlet component or through a render function.
- 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
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.
You can pass specific props to the outlet component using outletProps
:
<AsSlot
outlet={<Panel />}
outletProps={{
title: 'Settings',
collapsible: true,
defaultExpanded: true,
}}
>
<SettingsForm />
</AsSlot>
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>
Additional props are passed to the children via AsInstance
:
<AsSlot outlet={<Card />} className="highlighted" data-testid="user-card">
<UserProfile />
</AsSlot>
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>
Prop | Type | Description |
---|---|---|
children |
ReactNode | Content to render inside the outlet |
outlet |
ReactNode | RenderFunction | 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 ObjectProps = Record<string, any>;
type RenderFunction<Props> = (props: Props) => ReactNode;
- 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
is a declarative conditional rendering component that simplifies rendering different content based on conditions, with support for fallback content.
- 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
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>
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>
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>
If you don't provide a fallback, nothing is rendered when the condition is falsy:
<Condition when={showBanner}>
<AnnouncementBanner message={bannerText} />
</Condition>
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>
);
}
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 |
- 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
is a utility component that simplifies handling empty or undefined children by rendering fallback content when needed.
- 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
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.
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>
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>
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>
);
}
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 |
- Displaying placeholders when data is not available
- Creating components with meaningful empty states
- Building more resilient UI components
- Simplifying conditional rendering in JSX
The createEvent
function creates custom events for the Compositor system with full support for event bubbling, propagation control, and default action prevention.
import { createEvent } from '@ffsmio/compositor';
// Create a basic event
const myEvent = createEvent('button-click', { id: 'submit-button' });
// Use the event
element.dispatchEvent(myEvent);
// 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 });
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
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 |
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>
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>
);
}
- 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
- Supports all modern browsers
- IE11 compatible with appropriate polyfills
- Works in both client-side and server-side rendering environments
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
This project is licensed under the MIT License - see the LICENSE file for details.