@ffsm/factory
@ffsm/factory
is a powerful component factory for React that simplifies the creation of reusable components with built-in support for composition patterns, conditional rendering, and prop management.
Note: Additional examples and illustrations will be added in upcoming updates.
Table of Contents
- Introduction
- Installation
- Core Features
- Basic Usage
- Component Factory
- Compositor Integration
- Integration with Tailwind CSS
- API Reference
- Type System
- Utilities
- Frequently Asked Questions
- Why Choose @ffsm/factory?
- Performance Considerations
- Compatibility
- Related Packages
Introduction
@ffsm/factory
is a powerful component creation system for React that streamlines how you build, compose,
and manage reusable UI components. Unlike traditional component libraries, factory doesn't impose design opinions
but instead focuses on providing a flexible and type-safe foundation for creating your own component ecosystem.
At its core, @ffsm/factory
solves common challenges in component development:
- Component Creation Overhead: Simplifies creating consistent components with less boilerplate
- Prop Management Complexity: Handles prop forwarding, filtering, and merging automatically
- Type Safety Challenges: Provides comprehensive TypeScript support with proper generic typing
- Composition Patterns: Offers built-in patterns for component composition
- UI Pattern Repetition: Consolidates common UI patterns into declarative APIs
The library comes in two versions:
- Basic Factory (
@ffsm/factory
): Core functionality for component creation, styling, and prop management - Compositor Factory (
@ffsm/factory/compositor
): Extended version with advanced UI patterns like conditional rendering, slots, and empty states
Installation
Installing @ffsm/factory
is straightforward with your preferred package manager:
# Using npm
npm install @ffsm/factory
# Using yarn
yarn add @ffsm/factory
# Using pnpm
pnpm add @ffsm/factory
Basic vs. Compositor Installation
@ffsm/factory
comes in two versions:
- Basic Factory: The core package with essential component creation features
import { factory } from '@ffsm/factory';
- Compositor-Enabled Factory: Extended version with advanced UI patterns
# Install both packages for compositor features
npm install @ffsm/factory @ffsm/compositor
import { factory } from '@ffsm/factory/compositor';
TypeScript Support
@ffsm/factory
includes built-in TypeScript types - no additional packages required.
Peer Dependencies
- React 16.8+ (for Hooks support)
- React DOM 16.8+
Optional Integrations
- Tailwind CSS: Works excellently with Tailwind for utility-first styling
- CSS Modules: Fully compatible with CSS Modules and other styling approaches
- Other UI Libraries: Can wrap components from any React UI library
Choose the installation that matches your requirements - start with the basic package for simple component creation, or include the compositor package for advanced UI patterns.
Core Features
@ffsm/factory
provides a comprehensive set of features to streamline React component development:
- Component Creation: Easily create reusable components with consistent APIs
- Prop Management: Smart prop merging and filtering with type safety
- Ref Forwarding: Automatic ref handling for all created components
- TypeScript Integration: Full TypeScript support with proper generic types
- Flexible API: Support for static and dynamic initialization
- Template Functions: Customize component rendering with template functions
- Composition Patterns: Built-in support for advanced UI patterns (with compositor)
Basic Factory Features
The core factory functionality focuses on simplified component creation:
import { factory } from '@ffsm/factory';
// Create a simple button component
const Button = factory<{}>('Button', {
className: 'btn',
as: 'button',
});
// Use it in your app
function App() {
return (
<Button className="btn-primary" onClick={handleClick}>
Click Me
</Button>
);
}
Compositor Integration Features
When used with the compositor package, additional UI patterns are available:
- Conditional Rendering: Show components based on conditions or state
- Empty State Handling: Display fallback content when children are empty
- Slot-Based Composition: Render children into specific locations in a component
- Node-Based Rendering: Conditionally render based on children existence
import { factory } from '@ffsm/factory/compositor';
// Component that only renders for authenticated users
const ProtectedContent = factory<{}>('ProtectedContent', {
condition: (props) => props.isAuthenticated,
conditionFallback: <LoginPrompt />,
});
// Usage
<ProtectedContent isAuthenticated={user?.authenticated}>
<UserDashboard />
</ProtectedContent>;
Advanced Prop Management
Factory components automatically handle:
- Prop Forwarding: Pass appropriate props to the DOM or wrapped components
- Prop Merging: Intelligently combine className, style and other props
- Prop Filtering: Exclude specific props from being forwarded to DOM elements
- Custom Prop Logic: Control exactly which props get forwarded and how
All these features are designed to work together seamlessly, providing a robust foundation for building React component libraries or design systems.
Basic Usage
Getting started with @ffsm/factory
is simple and straightforward. The library provides an intuitive API
for creating reusable React components with minimal boilerplate:
import { factory } from '@ffsm/factory';
// Create a simple button component
const Button = factory<{}>('Button', {
className: 'btn',
as: 'button',
});
// Use it in your application
function App() {
return (
<Button className="btn-primary" onClick={() => alert('Clicked!')}>
Click Me
</Button>
);
}
Creating Components
The factory()
function takes three parameters:
- Display Name: Used in React DevTools for easier debugging
- Initial Props (optional): Static props or a function returning props
- Options (optional): Additional configuration for the component
// Static props initialization
const Card = factory<{}>('Card', {
className: 'card',
as: 'div',
});
// Dynamic props based on component props
const Button = factory<{ variant?: 'primary' | 'secondary' }>(
'Button',
(props) => ({
className: `btn ${props.variant ? `btn-${props.variant}` : ''}`,
as: 'button',
})
);
Using Factory Components
Factory components behave like standard React components:
// Props are merged with initial props
<Button className="mt-4" onClick={handleClick}>
Submit
</Button>;
// Props can be spread
const buttonProps = { className: 'large', disabled: true };
<Button {...buttonProps}>Cancel</Button>;
// Ref forwarding works automatically
const buttonRef = useRef < HTMLButtonElement > null;
<Button ref={buttonRef}>Focus Me</Button>;
Customizing Element Type
You can specify the element type using the as
prop:
// Create a component with default element type
const Text = factory<{}>('Text', {
className: 'text',
});
// Override element type when using the component
<Text as="h1" className="text-xl">Heading</Text>
<Text as="p" className="text-sm">Paragraph</Text>
<Text as="span" className="text-xs">Small text</Text>
Type Safety
Factory components are fully typed with TypeScript, providing excellent autocomplete and type checking:
// Define component-specific props
type ButtonProps = {
variant?: 'primary' | 'secondary' | 'outline';
size?: 'sm' | 'md' | 'lg';
};
// Create type-safe component
const Button = factory<ButtonProps>('Button', (props) => ({
className: `btn btn-${props.variant || 'primary'} btn-${props.size || 'md'}`,
as: 'button',
}));
// Type checking and autocomplete work as expected
<Button
variant="primary" // ✓ Autocomplete for 'primary', 'secondary', 'outline'
size="lg" // ✓ Autocomplete for 'sm', 'md', 'lg'
onClick={() => {}} // ✓ Standard button props available
/>;
These examples demonstrate the foundation of using @ffsm/factory
for component creation.
The subsequent sections will explore more advanced patterns and features.
Component Factory
The component factory is the heart of @ffsm/factory
, providing a flexible and type-safe way to
create reusable React components. This section explores its capabilities, from basic usage to advanced
configuration options.
Creating Components with Factory
The factory()
function streamlines the process of creating React components by handling boilerplate
code and providing a consistent API:
import { factory } from '@ffsm/factory';
// Create a simple button component
const Button = factory<{}>('Button', {
className: 'btn',
as: 'button',
});
// Create a card component
const Card = factory<{}>('Card', {
className: 'card',
});
When you create a component with factory()
, it:
- Creates a forward-ref component with the specified display name
- Handles prop merging (className, style, etc.)
- Sets up proper type definitions with TypeScript
- Automatically forwards refs to the underlying DOM element
Dynamic Props Initialization
One of the most powerful features of the factory is dynamic prop initialization, which allows you to define props based on the component's runtime props:
import { factory } from '@ffsm/factory';
// Button component with variant support
const Button = factory<{
variant?: 'primary' | 'secondary' | 'outline';
size?: 'sm' | 'md' | 'lg';
}>('Button', (props) => ({
className: `
btn
${props.variant ? `btn-${props.variant}` : 'btn-primary'}
${props.size ? `btn-${props.size}` : 'btn-md'}
`,
as: 'button',
}));
// Usage
<Button variant="secondary" size="lg">
Large Secondary Button
</Button>;
This approach enables:
- Prop-driven styling: Change component appearance based on props
- Conditional attributes: Add or remove attributes based on prop values
- Computed props: Derive props from multiple input props
Advanced Configuration Options
The third parameter to factory()
lets you configure how the component handles props:
import { factory } from '@ffsm/factory';
// Create a link component with external link handling
const Link = factory<{
isExternal?: boolean;
}>(
'Link',
{
as: 'a',
className: 'link',
},
{
// Don't forward custom props to the DOM
excludeProps: ['isExternal'],
// Custom template function to add security attributes to external links
template: (Component, props, initProps) => (
<Component
{...props}
target={initProps.isExternal ? '_blank' : undefined}
rel={initProps.isExternal ? 'noopener noreferrer' : undefined}
/>
),
}
);
// Usage
<Link href="https://example.com" isExternal>
External Link
</Link>;
Available Options
The factory accepts several configuration options to customize behavior:
- excludeProps: Array of prop names that shouldn't be forwarded to DOM
- shouldForwardProp: Function to determine if a prop should be forwarded
- template: Custom rendering template for more complex component structures
Custom Templates
The template option gives you complete control over how your component renders:
// Create a form field with label and error handling
const FormField = factory<{
label?: string;
error?: string;
}>(
'FormField',
{
as: 'input',
className: 'form-control',
},
{
excludeProps: ['label', 'error'],
template: (Component, props, initProps) => (
<div className="form-group">
{initProps.label && (
<label className="form-label">{initProps.label}</label>
)}
<Component {...props} />
{initProps.error && <div className="form-error">{initProps.error}</div>}
</div>
),
}
);
// Usage
<FormField
label="Email Address"
type="email"
placeholder="your@email.com"
error={errors.email}
/>;
Templates are useful for:
- Creating layout structures around components
- Adding decorative elements based on props
- Implementing complex interaction patterns
- Creating compound components with multiple parts
By leveraging the configuration options, you can create highly customized and reusable components while maintaining clean, developer-friendly APIs.
Compositor Integration
The Compositor integration extends @ffsm/factory
with advanced UI patterns leveraging
the @ffsm/compositor
package. This powerful combination allows you to create components
with built-in support for conditional rendering, slot-based composition, and empty state handling.
Installation
To use the compositor features, you need to install both packages:
# Using npm
npm install @ffsm/factory @ffsm/compositor
# Using yarn
yarn add @ffsm/factory @ffsm/compositor
# Using pnpm
pnpm add @ffsm/factory @ffsm/compositor
Usage
Import factory from the compositor subpath to access all the enhanced features:
// Import the compositor-enabled factory
import { factory } from '@ffsm/factory/compositor';
// Create components with advanced composition patterns
const Card = factory<{}>('Card', {
className: 'card',
emptyFallback: <p>No content available</p>,
});
Slot-Based Composition
The slot-based composition pattern allows you to render children into designated "slots" within your component, similar to how web components handle content projection:
import { factory } from '@ffsm/factory/compositor';
// Create a dialog with header and content slots
const Dialog = factory<{}>('Dialog', {
className: 'dialog',
asSlot: true,
children: ({ children }) => (
<div className="dialog-container">
<div className="dialog-header"></div>
<div className="dialog-content">{children}</div>
</div>
),
});
// Usage with content projected into the slot
<Dialog>This content will be rendered inside the dialog-content div</Dialog>;
Conditional Rendering
Conditional rendering lets you show or hide components based on specific conditions:
import { factory } from '@ffsm/factory/compositor';
// Component that only renders for authenticated users
const ProtectedContent = factory<{
isAuthenticated: boolean;
}>('ProtectedContent', (props) => ({
condition: props.isAuthenticated,
conditionFallback: <LoginPrompt />,
}));
// Component with async conditions (e.g., permission check)
const AdminPanel = factory<{
userId: string;
}>('AdminPanel', (props) => ({
// Can return a Promise for async checks
condition: async () => {
const permissions = await fetchUserPermissions(props.userId);
return permissions.includes('admin');
},
conditionFallback: <AccessDenied />,
}));
// Usage
<ProtectedContent isAuthenticated={Boolean(user)}>
<UserDashboard />
</ProtectedContent>;
Empty State Handling
Empty state handling provides fallback content when children are empty or non-existent:
import { factory } from '@ffsm/factory/compositor';
// List with empty state
const UserList = factory<{}>('UserList', {
as: 'ul',
className: 'user-list',
emptyFallback: <p className="empty-message">No users found</p>,
});
// Usage with conditional content
<UserList>
{users.length > 0
? users.map((user) => <li key={user.id}>{user.name}</li>)
: null}
</UserList>;
You can also use the asNode
option to only render a component when it has children:
import { factory } from '@ffsm/factory/compositor';
// Section that only renders when it has content
const Section = factory<{}>('Section', {
className: 'section',
asNode: true,
});
// Won't render anything
<Section />
// Will render normally
<Section>
<h2>Section Title</h2>
<p>Content here</p>
</Section>
Combined Features
The real power of compositor integration comes from combining these patterns:
import { factory } from '@ffsm/factory/compositor';
// Data panel component with loading, empty, and error states
const DataPanel = factory<{
isLoading: boolean;
error?: string;
}>('DataPanel', (props) => ({
// Only render when not loading
condition: !props.isLoading,
conditionFallback: <LoadingSpinner />,
// Show error message when there's an error
asSlot: Boolean(props.error),
children: props.error ? (
<div className="error-container">{props.error}</div>
) : undefined,
// When no error but no content, show empty state
emptyFallback: <EmptyState message="No data available" />,
}));
// Usage in a data-fetching scenario
<DataPanel isLoading={loading} error={error}>
{data &&
data.items.map((item) => (
<div key={item.id} className="data-item">
{item.name}
</div>
))}
</DataPanel>;
The compositor integration eliminates the need for repetitive conditional rendering patterns, empty state checks, and nested component structures, resulting in cleaner and more maintainable code.
By leveraging these patterns, you can create sophisticated UI components with minimal code while maintaining a clean, declarative API that's easy for other developers to use.
Integration with Tailwind CSS
@ffsm/factory
works exceptionally well with Tailwind CSS, allowing you to create reusable
components with consistent styling while maintaining the utility-first approach.
Creating Tailwind Component Libraries
Using factory with Tailwind CSS gives you the best of both worlds: the simplicity of Tailwind's utility classes and the reusability of component abstractions:
import { factory, clsx } from '@ffsm/factory';
// Create a reusable button component with Tailwind classes
export const Button = factory<{
variant?: 'primary' | 'secondary' | 'outline';
size?: 'sm' | 'md' | 'lg';
}>('Button', (props) => ({
as: 'button',
className: clsx(
// Base styles
'font-medium rounded transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2',
// Size variations
props.size === 'sm' && 'px-3 py-1.5 text-sm',
props.size === 'lg' && 'px-5 py-2.5 text-lg',
(!props.size || props.size === 'md') && 'px-4 py-2 text-base',
// Variant styles
props.variant === 'primary' &&
'bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500',
props.variant === 'secondary' &&
'bg-gray-600 text-white hover:bg-gray-700 focus:ring-gray-500',
props.variant === 'outline' &&
'border border-gray-300 text-gray-700 hover:bg-gray-50 focus:ring-gray-500',
(!props.variant || props.variant === 'primary') &&
'bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500'
),
}));
// Usage
<Button variant="secondary" size="lg" onClick={handleClick}>
Save Changes
</Button>;
Building UI Systems with Tailwind + Factory
You can create complete UI systems by composing factory components with Tailwind classes:
// Card components
export const Card = factory<{}>('Card', {
className: 'bg-white dark:bg-gray-800 rounded-lg shadow overflow-hidden',
});
export const CardHeader = factory<{}>('CardHeader', {
className: 'px-4 py-5 sm:px-6 border-b border-gray-200 dark:border-gray-700',
});
export const CardBody = factory<{}>('CardBody', {
className: 'px-4 py-5 sm:p-6',
});
export const CardFooter = factory<{}>('CardFooter', {
className: 'px-4 py-4 sm:px-6 border-t border-gray-200 dark:border-gray-700',
});
// Form components
export const FormGroup = factory<{}>('FormGroup', {
className: 'mb-4',
});
export const Label = factory<{ required?: boolean }>('Label', (props) => ({
as: 'label',
className: 'block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1',
children: props.children,
}));
export const Input = factory<{
error?: boolean;
}>('Input', (props) => ({
as: 'input',
className: clsx(
'block w-full rounded-md shadow-sm sm:text-sm',
props.error
? 'border-red-300 text-red-900 placeholder-red-300 focus:border-red-500 focus:ring-red-500'
: 'border-gray-300 focus:border-blue-500 focus:ring-blue-500'
),
}));
Advantages Over Direct Tailwind Usage
Using factory with Tailwind provides several advantages:
- Consistency: Define variants and styles in one place to ensure consistent UI
- DRY Principle: Avoid repeating the same lengthy class strings throughout your application
- Maintainability: Update styling in one place rather than hunting through your codebase
- Type Safety: Get full TypeScript support for component variations
- Prop-Based Styling: Toggle styles based on props rather than conditional class composition
- Semantic Markup: Create semantically meaningful components instead of div-soup with classes
With Tailwind Plugins
Factory components work seamlessly with Tailwind plugins like @tailwindcss/forms
:
export const Select = factory<{
error?: boolean;
}>('Select', (props) => ({
as: 'select',
className: clsx(
'block w-full rounded-md shadow-sm sm:text-sm',
props.error
? 'border-red-300 text-red-900 focus:border-red-500 focus:ring-red-500'
: 'border-gray-300 focus:border-blue-500 focus:ring-blue-500'
),
}));
Advanced Tailwind Integration
For more complex scenarios, you can leverage dynamic props with Tailwind:
import { factory, clsx } from '@ffsm/factory';
// Text component with color, size and weight variants
export const Text = factory<{
color?: 'primary' | 'secondary' | 'error' | 'success';
size?: 'xs' | 'sm' | 'base' | 'lg' | 'xl' | '2xl';
weight?: 'normal' | 'medium' | 'semibold' | 'bold';
}>('Text', (props) => ({
className: clsx(
// Size mapping
props.size === 'xs' && 'text-xs',
props.size === 'sm' && 'text-sm',
props.size === 'base' && 'text-base',
props.size === 'lg' && 'text-lg',
props.size === 'xl' && 'text-xl',
props.size === '2xl' && 'text-2xl',
(!props.size || props.size === 'base') && 'text-base',
// Weight mapping
props.weight === 'normal' && 'font-normal',
props.weight === 'medium' && 'font-medium',
props.weight === 'semibold' && 'font-semibold',
props.weight === 'bold' && 'font-bold',
(!props.weight || props.weight === 'normal') && 'font-normal',
// Color mapping
props.color === 'primary' && 'text-blue-600 dark:text-blue-400',
props.color === 'secondary' && 'text-gray-600 dark:text-gray-400',
props.color === 'error' && 'text-red-600 dark:text-red-400',
props.color === 'success' && 'text-green-600 dark:text-green-400',
(!props.color || props.color === 'primary') &&
'text-gray-900 dark:text-white'
),
}));
// Usage
<Text size="lg" color="error" weight="bold">
Something went wrong!
</Text>;
This approach gives you all the benefits of Tailwind's utility-first approach while providing the abstraction and reusability of a component library.
API Reference
factory()
The core factory function for creating components with standardized patterns and prop handling.
// Basic factory
function factory<
AdditionalProps extends Record<string, any>,
Element extends ElementType = 'div',
>(
displayName: string,
init?: InitialProps<Element, AdditionalProps>,
options?: FactoryOptions<Element, AdditionalProps>
): ForwardRefExoticComponent<
PropsWithoutRef<FactoryProps<Element, AdditionalProps>> &
RefAttributes<Element>
>;
// Compositor-enabled factory
// From '@ffsm/factory/compositor'
function factory<
AdditionalProps extends Record<string, any>,
Element extends ElementType = 'div',
>(
displayName: string,
init?: InitialProps<Element, AdditionalProps>,
options?: CompositorFactoryOptions<Element, AdditionalProps>
): ForwardRefExoticComponent<
PropsWithoutRef<FactoryProps<Element, AdditionalProps>> &
RefAttributes<Element>
>;
Parameters
displayName
(string): The display name for the component in React DevToolsinit
(optional): Initial props or function that returns props based on component propsoptions
(optional): Configuration options for the factory component
Returns
A forward ref React component with the specified props and features.
Init Props
The second parameter (init) of the factory function can be either an object or a function that returns an object. It accepts the following properties:
Prop | Type | Description |
---|---|---|
as |
ElementType |
The base element type to render (div, button, etc.) |
className |
string , (props) => string |
Static or dynamic class name |
style |
CSSProperties , (props) => CSSProperties |
Static or dynamic styles |
children |
ReactNode , (props) => ReactNode |
Static content or render function |
...props |
any |
Any additional props to pass to the component |
Usage Examples
Static initialization:
const Card = factory<{}>('Card', {
className: 'card p-4 rounded shadow',
as: 'div',
});
Dynamic initialization:
const Button = factory<{ variant?: 'primary' | 'secondary' }>(
'Button',
(props) => ({
className: `btn ${props.variant ? `btn-${props.variant}` : 'btn-primary'}`,
as: 'button',
})
);
Options
The third parameter (options
) allows you to configure advanced behavior of your component:
Basic Factory Options
Option | Type | Description |
---|---|---|
excludeProps |
Array<string> |
Props to exclude from being forwarded to DOM |
shouldForwardProp |
(key: keyof Props) => boolean |
Custom function to determine if a prop should be forwarded |
template |
(Component, props, initProps) => ReactNode |
Custom rendering template for the component |
Compositor Factory Options (available in '@ffsm/factory/compositor')
Option | Type | Description |
---|---|---|
asSlot |
boolean |
Enable slot-based composition |
asNode |
boolean |
Render only when children exist |
asNodeFalsy |
boolean |
Use strict falsy checking for asNode |
emptyFallback |
ReactNode |
Content to show when children are empty |
condition |
unknown , (props) => unknown |
Condition for conditional rendering |
conditionFallback |
ReactNode |
Content to show when condition is falsy |
conditionFalsy |
boolean |
Use strict falsy checking for condition |
Usage Examples
Excluding custom props:
const Link = factory<{ isExternal?: boolean }>(
'Link',
{
as: 'a',
className: 'link',
},
{
excludeProps: ['isExternal'],
}
);
Custom template:
const FormField = factory<{ label?: string; error?: string }>(
'FormField',
{
as: 'input',
className: 'form-input',
},
{
excludeProps: ['label', 'error'],
template: (Component, props, initProps) => (
<div className="form-group">
{initProps.label && <label>{initProps.label}</label>}
<Component {...props} />
{initProps.error && <div className="error">{initProps.error}</div>}
</div>
),
}
);
Compositor features:
import { factory } from '@ffsm/factory/compositor';
const ProtectedContent = factory<{ isAdmin: boolean }>(
'ProtectedContent',
{
className: 'protected-content',
},
{
condition: (props) => props.isAdmin,
conditionFallback: <AccessDenied />,
}
);
Types
The package exports several utility types for working with factory components:
Type | Description |
---|---|
FactoryProps<Element, AdditionalProps> |
Combined props type for factory components |
InitialProps<Element, AdditionalProps> |
Type for initial props or props factory function |
FactoryOptions<Element, AdditionalProps> |
Type for factory options (basic version) |
MaybeFn<Result, Props> |
Type that can be either a value or a function returning it |
ObjectProps |
Generic object properties type |
Utilities
clsx
Utility for conditionally joining class names:
import { clsx } from '@ffsm/factory';
const className = clsx(
'base-class',
condition && 'conditional-class',
{ 'object-key': booleanValue },
['array', 'of', 'classes']
);
Type System
The @ffsm/factory
package includes a comprehensive type system that provides full TypeScript support.
This section explains the key types and how to use them effectively in your components.
Core Types
FactoryProps<Element, AdditionalProps>
This is the main props type for factory-created components. It combines:
- Element-specific props (like href for an anchor)
- Your custom additional props
- Common factory props (className, style, etc.)
// Example: Creating a button with custom props
type ButtonProps = {
variant: 'primary' | 'secondary';
size: 'sm' | 'md' | 'lg';
};
const Button = factory<ButtonProps, 'button'>('Button', {
className: 'btn',
as: 'button',
});
// Usage with type checking:
<Button
variant="primary" // ✓ Type checked
size="sm" // ✓ Type checked
onClick={() => {}} // ✓ Inherited from 'button' element
invalid={true} // ✗ Type error
/>;
InitialProps<Element, AdditionalProps>
This flexible type represents initial props configuration and can be either:
- Static object:
const Card = factory<{}>('Card', {
className: 'card',
as: 'div',
});
- Function that returns props based on incoming props:
const Button = factory<ButtonProps>('Button', (props) => ({
className: `btn btn-${props.variant} btn-${props.size}`,
as: 'button',
}));
Using Generic Types
Custom Component Props
Define custom props with full type safety:
interface TabProps {
active?: boolean;
label: string;
index: number;
}
const Tab = factory<TabProps>('Tab', (props) => ({
className: props.active ? 'tab active' : 'tab',
'aria-selected': props.active,
role: 'tab',
as: 'div',
}));
// Usage with TypeScript validation
<Tab
label="Settings" // Required
index={2} // Required
active={true} // Optional
/>;
Element Type Customization
Specify a different base element type:
const NavLink = factory<{}, 'a'>('NavLink', {
className: 'nav-link',
as: 'a',
});
// Usage with anchor-specific props
<NavLink href="/about" target="_blank">
About
</NavLink>;
Advanced Type Features
Type Inference Best Practices
When creating a factory component without additional props, always specify an empty object explicitly:
// Correct approach - explicitly specify empty object
export const FormWrapper = factory<{}>('FormWrapper', {
className: 'w-full',
});
// Not recommended - may cause type inference issues
export const FormContainer = factory('FormContainer', {
className: 'flex w-full border border-white/20 rounded-lg',
});
Conditional Props
Create components with conditional prop requirements:
// Better approach using union types
type DialogProps =
| { title: string; size?: 'sm' | 'md' | 'lg'; closable: true; onClose: () => void }
| { title: string; size?: 'sm' | 'md' | 'lg'; closable?: false; onClose?: never };
const Dialog = factory<DialogProps>('Dialog', {
className: 'dialog',
});
// Usage:
// Valid - onClose is required when closable is true
<Dialog title="Settings" closable={true} onClose={() => setOpen(false)} />
// Valid - onClose is not allowed when closable is false
<Dialog title="Info" closable={false} />
// Invalid - missing onClose
<Dialog title="Error" closable={true} /> // TypeScript error
// Invalid - has onClose when closable is false
<Dialog title="Warning" closable={false} onClose={() => {}} /> // TypeScript error
Component Composition Types
Build complex component hierarchies with composed types:
type CardProps = {
title: string;
elevated?: boolean;
};
type CardImageProps = CardProps & {
imageSrc: string;
imageAlt?: string;
};
const Card = factory<CardProps>('Card', {
className: 'card',
});
const CardWithImage = factory<CardImageProps>('CardWithImage', (props) => ({
className: `card ${props.elevated ? 'elevated' : ''}`,
children: (
<>
<img src={props.imageSrc} alt={props.imageAlt || props.title} />
<h3>{props.title}</h3>
</>
),
}));
Utility Types
The package exports several utility types for advanced use cases:
MaybeFn<Result, Props>
: Type for values that can be either direct or function-returnedObjectProps
: Generic object properties typeFactoryProps<Element, AdditionalProps>
: Combined props for factory componentsFactoryOptions<Element, AdditionalProps>
: Options for factory configuration
Import these types directly from the package:
import { MaybeFn, ObjectProps, FactoryProps } from '@ffsm/factory';
// Create a type for a component that needs dynamic styling
type DynamicComponent<P = {}> = React.FC<
P & {
styling: MaybeFn<string, P>;
}
>;
By leveraging these type utilities, you can create type-safe component APIs that provide excellent developer experience with autocompletion and type checking.
Utilities
@ffsm/factory
includes several utility functions that help with common component development tasks.
These utilities can be imported directly from the package and used both within factory components
and in your application code.
clsx
The package includes a lightweight version of the clsx utility for conditional class name composition, allowing you to combine class names dynamically:
import { clsx } from '@ffsm/factory';
// Basic usage
const className = clsx(
'base-class',
condition && 'conditional-class',
isActive ? 'active' : 'inactive',
{ hidden: isHidden, visible: !isHidden }
);
// Within a factory component
const Button = factory<{
variant?: 'primary' | 'secondary';
size?: 'sm' | 'md' | 'lg';
disabled?: boolean;
}>('Button', (props) => ({
as: 'button',
className: clsx(
'btn',
props.variant && `btn-${props.variant}`,
props.size && `btn-${props.size}`,
props.disabled && 'btn-disabled'
),
}));
The clsx
utility supports:
- Strings:
'btn'
- Objects:
{ 'btn-primary': isPrimary }
- Arrays:
['btn', isActive && 'btn-active']
- Nested conditions:
condition && [subCondition && 'class']
- Falsy values are ignored:
false && 'hidden'
results in nothing being added
This eliminates the need for an additional package for class name management in your projects.
Frequently Asked Questions
How is @ffsm/factory different from styled-components?
While styled-components
focuses on styling with CSS-in-JS, @ffsm/factory
provides a more comprehensive
approach to component creation with built-in composition patterns, conditional rendering, and prop management.
Can I use @ffsm/factory with other UI libraries?
Yes, @ffsm/factory works with any React-based UI library. You can wrap components from Material UI, Chakra UI,
or any other library using factory()
.
Does it work with React Server Components?
Yes, @ffsm/factory
is compatible with React Server Components, but be aware that some dynamic features
might need client-side hydration.
What's the difference between the regular factory and compositor factory?
The regular factory (import { factory } from '@ffsm/factory'
) provides core functionality for component creation,
prop forwarding, and templates. The compositor factory (import { factory } from '@ffsm/factory/compositor'
) adds
support for advanced UI patterns like conditional rendering, slot-based composition, and empty state handling through
integration with @ffsm/compositor
.
When should I use the basic factory vs. compositor factory?
Use the basic factory when you only need component creation and prop management. Use the compositor factory when you need advanced UI patterns like conditional rendering, slots, or empty state handling. The basic factory has fewer dependencies and a smaller bundle size.
Does using the compositor features impact performance?
The compositor integration adds a small runtime overhead, but it's negligible for most applications. The benefits
of cleaner code and declarative patterns usually outweigh the minimal performance cost. For performance-critical
scenarios with large lists, consider memoizing components with React.memo
.
Do I need to install @ffsm/compositor separately?
Yes, when using the compositor features, you need to install both packages:
npm install @ffsm/factory @ffsm/compositor
The basic factory doesn't require the compositor package.
Why Choose @ffsm/factory?
While there are many component libraries and styling solutions available, @ffsm/factory
offers unique advantages
that set it apart:
Compared to UI Libraries
Traditional UI libraries like Material UI, Chakra UI, or Ant Design provide pre-built components with specific
design systems. @ffsm/factory
takes a different approach:
- Zero Design Opinions: Create components that match your exact design requirements without fighting against pre-existing styles
- Lightweight Core: No bloated dependencies or unused components - just the functionality you need
- Composable Building Blocks: Build your own design system from the ground up rather than adapting existing components
- Progressive Adoption: Start with a few components and gradually expand without committing to an entire UI framework
Compared to Styling Libraries
Unlike CSS-in-JS libraries like styled-components or emotion, @ffsm/factory
:
- Focuses on Component Logic: Handles not just styling but composition patterns, conditional rendering, and prop management
- Styling Agnostic: Works with any styling approach - CSS modules, Tailwind, utility classes, or vanilla CSS
- Reduced Boilerplate: Creates consistent components with fewer lines of code
- Declarative Patterns: Simplifies common UI patterns with prop-based APIs instead of imperative code
Compared to Component Utilities
Headless UI libraries like Radix UI or Headless UI provide unstyled components, but @ffsm/factory
:
- Offers Complete Control: Define both behavior and presentation in a unified API
- Simplifies Implementation: Less verbose than hook-based component creation patterns
- Integrates Advanced Patterns: Built-in support for slots, empty states, and conditional rendering
- Prioritizes Developer Experience: Consistent API with excellent TypeScript integration
Real-World Benefits
- Faster Development: Create new components in minutes instead of hours
- Consistent APIs: Establish patterns that all team members can follow
- Better Maintainability: Centralized component logic with clear separation of concerns
- Improved Type Safety: Full TypeScript support with proper generics and inference
- Reduced Bundle Size: Only include the functionality you need
- Flexible Adaptation: Works with your existing components, libraries, and styling solutions
@ffsm/factory
is ideal for teams building custom design systems, developers who need flexibility beyond existing UI libraries,
and projects where component consistency and maintainability are priorities.
Performance Considerations
@ffsm/factory
is designed with performance in mind, but there are some considerations to ensure optimal performance in your applications:
- Component Memoization: When rendering large lists of factory components, wrap them with React.memo to prevent unnecessary re-renders:
const OptimizedCard = React.memo(factory<CardProps>('Card', {...}));
- Bundle Size: The core factory package is lightweight, but including the compositor features will increase bundle size. Use code-splitting to only load the compositor features when needed:
// Dynamic import of compositor-enabled components
const AdminPanel = lazy(() => import('./AdminPanel'));
- Conditional Rendering: When using conditional rendering frequently, prefer the declarative compositor approach over imperative conditionals for better readability and maintainability.
Compatibility
@ffsm/factory
is designed to work across a wide range of React environments:
- React Versions: Compatible with React 16.8+ (requires hooks support)
- React Frameworks:
- Next.js (both Pages and App Router)
- Create React App
- Vite
- Remix
- Gatsby
- Any other React-based framework
- TypeScript: Full TypeScript support with proper typing and generics
- React Server Components: Compatible with React Server Components, though some dynamic features require client components
- Styling Solutions: Works with any styling approach:
- Tailwind CSS
- CSS Modules
- Styled Components / Emotion
- Plain CSS
- CSS-in-JS libraries
- Browsers: Supports all modern browsers without polyfills
Related Packages
Core Packages
@ffsm/compositor
: The companion package that provides the advanced composition utilities used by factory's compositor integration. Install this package to use the advanced features like slots, conditional rendering, and empty state handling.
These packages are designed to work together to provide a complete solution for building React applications, but each can be used independently according to your specific needs.