Components with Pulse
Pulse Framework uses a DOM-first approach to building components. Your JSX components return real DOM elements, not virtual representations.
The Pulse.Fn Type
Pulse.Fn is the type for Function Components in Pulse:
tsx
type Pulse.Fn<PROPS = Record<string, any>> = (
props: PROPS
) => Pulse.JSX.Element | nullBasic Component
tsx
import Pulse from 'pulse-framework';
const Greeting: Pulse.Fn = () => {
return <h1>Hello, Pulse Framework!</h1>;
};
// Mount to DOM - it's a real HTMLElement!
document.body.appendChild(<Greeting />);Component with State
tsx
const Counter: Pulse.Fn = () => {
const count = Pulse.signal(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => count(count() + 1)}>
Increment
</button>
</div>
);
};Props and TypeScript
Typed Props
Define props with TypeScript interfaces for full type safety:
tsx
interface ButtonProps {
label: string;
onClick: () => void;
variant?: 'primary' | 'secondary' | 'danger';
disabled?: boolean;
}
const Button: Pulse.Fn<ButtonProps> = ({
label,
onClick,
variant = 'primary',
disabled = false
}) => {
return (
<button
className={`btn btn-${variant}`}
onClick={onClick}
disabled={disabled}
>
{label}
</button>
);
};
// Usage with full type checking
<Button
label="Save"
onClick={() => console.log('Saved')}
variant="primary"
/>Props with Signals
Props can include signals for reactive communication:
tsx
import { Signal } from 'pulse-framework';
interface CounterDisplayProps {
count: Signal<number>;
label?: string;
}
const CounterDisplay: Pulse.Fn<CounterDisplayProps> = ({
count,
label = 'Count'
}) => {
return (
<div>
<span>{label}: </span>
<strong>{count}</strong>
</div>
);
};
// Usage
const App: Pulse.Fn = () => {
const count = Pulse.signal(0);
return (
<div>
<CounterDisplay count={count} label="Current" />
<button onClick={() => count(count() + 1)}>+</button>
</div>
);
};Optional and Default Props
tsx
interface CardProps {
title: string;
subtitle?: string;
className?: string;
elevation?: 'sm' | 'md' | 'lg';
}
const Card: Pulse.Fn<CardProps> = ({
title,
subtitle,
className = '',
elevation = 'md'
}) => {
return (
<div className={`card card-${elevation} ${className}`}>
<h3>{title}</h3>
{subtitle && <p>{subtitle}</p>}
</div>
);
};Children and Composition
Basic Children
tsx
interface CardProps {
title: string;
children?: any;
}
const Card: Pulse.Fn<CardProps> = ({ title, children }) => {
return (
<div className="card">
<div className="card-header">
<h3>{title}</h3>
</div>
<div className="card-body">
{children}
</div>
</div>
);
};
// Usage
<Card title="User Profile">
<p>Name: John Doe</p>
<p>Email: john@example.com</p>
<button>Edit</button>
</Card>Multiple Children Slots
tsx
interface LayoutProps {
header?: any;
sidebar?: any;
children?: any;
footer?: any;
}
const Layout: Pulse.Fn<LayoutProps> = ({
header,
sidebar,
children,
footer
}) => {
return (
<div className="layout">
{header && <header>{header}</header>}
<div className="layout-main">
{sidebar && <aside>{sidebar}</aside>}
<main>{children}</main>
</div>
{footer && <footer>{footer}</footer>}
</div>
);
};
// Usage
<Layout
header={<h1>My App</h1>}
sidebar={<nav>...</nav>}
footer={<p>© 2024</p>}
>
<p>Main content here</p>
</Layout>Fragments
Fragments let you group elements without adding wrapper DOM nodes:
tsx
const List: Pulse.Fn = () => {
return (
<>
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
</>
);
};
// Or explicit Fragment
import { Fragment } from 'pulse-framework';
const List: Pulse.Fn = () => {
return (
<Fragment>
<li>Item 1</li>
<li>Item 2</li>
</Fragment>
);
};Conditional Rendering
With Computed
tsx
const LoginStatus: Pulse.Fn = () => {
const isLoggedIn = Pulse.signal(false);
const username = Pulse.signal('Guest');
return (
<div>
{Pulse.computed(() =>
isLoggedIn() ? (
<div>
<p>Welcome, {username}!</p>
<button onClick={() => isLoggedIn(false)}>Logout</button>
</div>
) : (
<div>
<p>Please sign in</p>
<button onClick={() => {
isLoggedIn(true);
username('John Doe');
}}>Login</button>
</div>
)
)}
</div>
);
};Multiple Conditions
tsx
const StatusView: Pulse.Fn = () => {
const status = Pulse.signal<'loading' | 'success' | 'error'>('loading');
return (
<div>
{Pulse.computed(() => {
switch (status()) {
case 'loading':
return <div>Loading...</div>;
case 'success':
return <div>Success!</div>;
case 'error':
return <div>Error occurred</div>;
}
})}
</div>
);
};Dynamic Lists
Rendering Lists
Wrap .map() in Pulse.computed() for reactive lists:
tsx
interface User {
id: number;
name: string;
email: string;
}
const UserList: Pulse.Fn = () => {
const users = Pulse.signal<User[]>([
{ id: 1, name: 'Alice', email: 'alice@example.com' },
{ id: 2, name: 'Bob', email: 'bob@example.com' }
]);
return (
<ul>
{Pulse.computed(() =>
users().map(user => (
<li key={user.id}>
{user.name} - {user.email}
</li>
))
)}
</ul>
);
};Filtered Lists
tsx
const FilteredList: Pulse.Fn = () => {
const items = Pulse.signal(['Apple', 'Banana', 'Cherry', 'Date']);
const filter = Pulse.signal('');
const filteredItems = Pulse.computed(() => {
const search = filter().toLowerCase();
if (!search) return items();
return items().filter(item => item.toLowerCase().includes(search));
});
return (
<div>
<input
type="text"
value={filter}
onInput={(e) => filter((e.target as HTMLInputElement).value)}
placeholder="Search..."
/>
<ul>
{Pulse.computed(() =>
filteredItems().map(item => (
<li key={item}>{item}</li>
))
)}
</ul>
</div>
);
};Event Handling
Basic Events
tsx
const Button: Pulse.Fn = () => {
const handleClick = () => {
console.log('Button clicked!');
};
return <button onClick={handleClick}>Click me</button>;
};Events with Parameters
tsx
const TodoList: Pulse.Fn = () => {
const todos = Pulse.signal([
{ id: 1, text: 'Learn Pulse' },
{ id: 2, text: 'Build app' }
]);
const removeTodo = (id: number) => {
todos(todos().filter(t => t.id !== id));
};
return (
<ul>
{Pulse.computed(() =>
todos().map(todo => (
<li key={todo.id}>
{todo.text}
<button onClick={() => removeTodo(todo.id)}>Remove</button>
</li>
))
)}
</ul>
);
};Keyboard Events
tsx
const SearchInput: Pulse.Fn = () => {
const query = Pulse.signal('');
const handleKeyPress = (e: KeyboardEvent) => {
if (e.key === 'Enter') {
console.log('Searching for:', query());
}
};
return (
<input
type="text"
value={query}
onInput={(e) => query((e.target as HTMLInputElement).value)}
onKeyPress={handleKeyPress}
/>
);
};Lifecycle with Effects
Component Mount
tsx
const DataLoader: Pulse.Fn = () => {
const data = Pulse.signal(null);
const loading = Pulse.signal(true);
// Runs on mount
Pulse.effect(() => {
fetch('/api/data')
.then(res => res.json())
.then(result => {
data(result);
loading(false);
});
});
return (
<div>
{Pulse.computed(() =>
loading() ? <p>Loading...</p> : <pre>{JSON.stringify(data(), null, 2)}</pre>
)}
</div>
);
};Cleanup
tsx
const Timer: Pulse.Fn = () => {
const seconds = Pulse.signal(0);
const isRunning = Pulse.signal(true);
Pulse.effect(() => {
if (!isRunning()) return;
const interval = setInterval(() => {
seconds(seconds() + 1);
}, 1000);
// Cleanup function
return () => clearInterval(interval);
});
return (
<div>
<p>Time: {seconds}s</p>
<button onClick={() => isRunning(!isRunning())}>
{Pulse.computed(() => isRunning() ? 'Pause' : 'Resume')}
</button>
</div>
);
};DOM References with Refs
Creating Refs
Use createRef() to get direct access to DOM elements for imperative operations:
tsx
import Pulse, { createRef } from 'pulse-framework';
const FocusInput: Pulse.Fn = () => {
const inputRef = createRef<HTMLInputElement>();
const handleFocus = () => {
inputRef.current?.focus();
};
return (
<div>
<input ref={inputRef.callback} type="text" placeholder="Enter text..." />
<button onClick={handleFocus}>Focus Input</button>
</div>
);
};Ref API
typescript
interface Ref<T extends HTMLElement = HTMLElement> {
current: T | null; // The DOM element (null before mount)
callback: (el: T | null) => void; // Pass to JSX ref attribute
}When to Use Refs
✅ Use refs for:
- Focus management (
element.focus()) - Measuring DOM (dimensions, scroll position)
- Triggering animations
- Integrating third-party libraries
- Scrolling to elements
❌ Don't use refs for:
- Updating content (use signals instead)
- Changing classes (use reactive bindings)
- Managing state (use signals)
Scroll to Element
tsx
const ScrollToSection: Pulse.Fn = () => {
const sectionRef = createRef<HTMLDivElement>();
const scrollToSection = () => {
sectionRef.current?.scrollIntoView({
behavior: 'smooth',
block: 'start'
});
};
return (
<div>
<button onClick={scrollToSection}>Scroll Down</button>
<div style={{ height: '150vh' }}>
<p>Scroll down to see the section...</p>
</div>
<div ref={sectionRef.callback} style={{ padding: '2rem', background: '#f0f0f0' }}>
<h2>Target Section</h2>
<p>You scrolled here!</p>
</div>
</div>
);
};Refs with Effects
Combine refs with effects for advanced DOM interactions:
tsx
const AutoFocusModal: Pulse.Fn<{ onClose: () => void }> = ({ onClose }) => {
const modalRef = createRef<HTMLDivElement>();
const closeButtonRef = createRef<HTMLButtonElement>();
Pulse.effect(() => {
// Focus first button when modal mounts
closeButtonRef.current?.focus();
// Trap focus inside modal
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape') onClose();
};
document.addEventListener('keydown', handleKeyDown);
return () => {
document.removeEventListener('keydown', handleKeyDown);
};
});
return (
<div ref={modalRef.callback} className="modal">
<div className="modal-content">
<h2>Modal Title</h2>
<p>Modal content here...</p>
<button ref={closeButtonRef.callback} onClick={onClose}>
Close
</button>
</div>
</div>
);
};Measuring Elements
tsx
const DimensionTracker: Pulse.Fn = () => {
const boxRef = createRef<HTMLDivElement>();
const dimensions = Pulse.signal({ width: 0, height: 0 });
const measure = () => {
const element = boxRef.current;
if (element) {
const rect = element.getBoundingClientRect();
dimensions({ width: rect.width, height: rect.height });
}
};
return (
<div>
<div
ref={boxRef.callback}
style={{
width: '200px',
height: '100px',
background: '#e0e0e0',
padding: '1rem'
}}
>
Resize me!
</div>
<button onClick={measure}>Measure</button>
<p>
Width: {Pulse.computed(() => dimensions().width)}px,
Height: {Pulse.computed(() => dimensions().height)}px
</p>
</div>
);
};TIP
For complete ref documentation including advanced patterns, see the Pulse Framework Refs Guide.
Styling Components
Static Classes
tsx
const Alert: Pulse.Fn = () => {
return (
<div className="alert alert-success">
Success message!
</div>
);
};Dynamic Classes
tsx
interface ButtonProps {
variant: 'primary' | 'secondary' | 'danger';
active?: boolean;
}
const Button: Pulse.Fn<ButtonProps> = ({ variant, active = false }) => {
const className = Pulse.computed(() => {
const classes = ['btn', `btn-${variant}`];
if (active) classes.push('btn-active');
return classes.join(' ');
});
return <button className={className}>Click me</button>;
};Inline Styles
tsx
const Box: Pulse.Fn = () => {
const width = Pulse.signal(100);
const color = Pulse.signal('#3b82f6');
const style = Pulse.computed(() => ({
width: `${width()}px`,
height: `${width()}px`,
backgroundColor: color(),
transition: 'all 0.3s ease'
}));
return (
<div>
<div style={style}></div>
<input
type="range"
min="50"
max="300"
value={width}
onInput={(e) => width(Number((e.target as HTMLInputElement).value))}
/>
</div>
);
};Communication Patterns
1. Shared Signals (Recommended)
tsx
// Shared state
const globalCount = Pulse.signal(0);
const Display: Pulse.Fn = () => {
return <div>Count: {globalCount}</div>;
};
const Controls: Pulse.Fn = () => {
return (
<div>
<button onClick={() => globalCount(globalCount() + 1)}>+</button>
<button onClick={() => globalCount(globalCount() - 1)}>-</button>
</div>
);
};
const App: Pulse.Fn = () => {
return (
<div>
<Display />
<Controls />
</div>
);
};2. Callback Props
tsx
interface FormProps {
onSubmit: (data: { email: string }) => void;
}
const EmailForm: Pulse.Fn<FormProps> = ({ onSubmit }) => {
const email = Pulse.signal('');
const handleSubmit = (e: Event) => {
e.preventDefault();
onSubmit({ email: email() });
};
return (
<form onSubmit={handleSubmit}>
<input
type="email"
value={email}
onInput={(e) => email((e.target as HTMLInputElement).value)}
/>
<button type="submit">Submit</button>
</form>
);
};
const App: Pulse.Fn = () => {
const handleFormSubmit = (data: { email: string }) => {
console.log('Email submitted:', data.email);
};
return <EmailForm onSubmit={handleFormSubmit} />;
};3. Props Drilling (Avoid)
tsx
// ❌ Avoid - props drilling through many layers
const GrandParent: Pulse.Fn = () => {
const count = Pulse.signal(0);
return <Parent count={count} />;
};
const Parent: Pulse.Fn<{ count: Signal<number> }> = ({ count }) => {
return <Child count={count} />;
};
const Child: Pulse.Fn<{ count: Signal<number> }> = ({ count }) => {
return <div>{count}</div>;
};
// ✅ Better - shared signal
const sharedCount = Pulse.signal(0);
const GrandParent: Pulse.Fn = () => <Parent />;
const Parent: Pulse.Fn = () => <Child />;
const Child: Pulse.Fn = () => <div>{sharedCount}</div>;Complete Example: Todo App
tsx
interface Todo {
id: number;
text: string;
completed: boolean;
}
const TodoApp: Pulse.Fn = () => {
const todos = Pulse.signal<Todo[]>([]);
const newTodoText = Pulse.signal('');
const remainingCount = Pulse.computed(() =>
todos().filter(t => !t.completed).length
);
const addTodo = () => {
const text = newTodoText().trim();
if (text) {
todos([...todos(), {
id: Date.now(),
text,
completed: false
}]);
newTodoText('');
}
};
const toggleTodo = (id: number) => {
todos(todos().map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
));
};
const removeTodo = (id: number) => {
todos(todos().filter(todo => todo.id !== id));
};
return (
<div className="todo-app">
<h1>Todo List</h1>
<div className="stats">
<p>{remainingCount} remaining</p>
</div>
<div className="input">
<input
type="text"
value={newTodoText}
onInput={(e) => newTodoText((e.target as HTMLInputElement).value)}
onKeyPress={(e) => e.key === 'Enter' && addTodo()}
placeholder="What needs to be done?"
/>
<button onClick={addTodo}>Add</button>
</div>
<ul className="todo-list">
{Pulse.computed(() =>
todos().map(todo => (
<li key={todo.id} className={todo.completed ? 'completed' : ''}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleTodo(todo.id)}
/>
<span>{todo.text}</span>
<button onClick={() => removeTodo(todo.id)}>✕</button>
</li>
))
)}
</ul>
</div>
);
};Best Practices
✅ DO: Use PascalCase
tsx
// ✅ Good
const UserCard: Pulse.Fn = () => { };
const NavBar: Pulse.Fn = () => { };
// ❌ Bad
const userCard: Pulse.Fn = () => { };
const navbar: Pulse.Fn = () => { };✅ DO: Type Your Props
tsx
// ✅ Good - type safe
interface ButtonProps {
label: string;
onClick: () => void;
}
const Button: Pulse.Fn<ButtonProps> = ({ label, onClick }) => { };
// ❌ Bad - no type safety
const Button: Pulse.Fn = (props: any) => { };✅ DO: Extract Complex Logic
tsx
// ✅ Good - reusable logic
function useValidation(value: Signal<string>) {
return Pulse.computed(() => {
const v = value();
if (!v) return 'Required';
if (v.length < 3) return 'Too short';
return null;
});
}
const Form: Pulse.Fn = () => {
const name = Pulse.signal('');
const nameError = useValidation(name);
// ...
};✅ DO: Keep Components Focused
tsx
// ✅ Good - single responsibility
const UserAvatar: Pulse.Fn = () => { };
const UserName: Pulse.Fn = () => { };
const UserBio: Pulse.Fn = () => { };
const UserProfile: Pulse.Fn = () => {
return (
<div>
<UserAvatar />
<UserName />
<UserBio />
</div>
);
};Next Steps
- Reactivity Guide — Master signals and computed values
- Advanced Patterns — Stores, routing, and complex state
- Browse Components — See real examples
Build amazing components! 🎨