Execute a function with batched updates. All signal writes during execution are queued and effects run once after completion.
import { batch } from 'pulsar-ui'
batch(() => {
setCount(1)
setName('Bob')
setAge(30)
})
// Effects run once, not three timesNested Batches:
batch(() => {
setCount(1)
batch(() => {
setName('Bob')
})
setAge(30)
})
// Still only runs effects once at the endCheck if currently in a batch context.
if (isBatching()) {
console.log('Updates are being batched')
}Conditional rendering component.
Props:
when: boolean | (() => boolean)- Condition to checkfallback?: HTMLElement | (() => HTMLElement)- Content when falsechildren: HTMLElement | (() => HTMLElement)- Content when true
Examples:
Basic usage:
<Show when={isLoggedIn()}>
<Dashboard />
</Show>With fallback:
<Show
when={isLoggedIn()}
fallback={<Login />}
>
<Dashboard />
</Show>Function children:
<Show when={() => count() > 5}>
{() => <HighCount count={count()} />}
</Show>List rendering component with optional key-based reconciliation.
Props:
each: T[] | (() => T[])- Array to iteratekey?: (item: T, index: number) => string | number- Key functionchildren: (item: T, index: () => number) => HTMLElement- Render functionfallback?: HTMLElement | (() => HTMLElement)- Empty state
Examples:
Simple list:
<For each={items()}>
{(item, index) => <div>{index()}: {item}</div>}
</For>With keys (efficient updates):
<For
each={todos()}
key={(todo) => todo.id}
>
{(todo) => <TodoItem todo={todo} />}
</For>With fallback:
<For
each={items()}
fallback={<EmptyState />}
>
{(item) => <ItemCard item={item} />}
</For>Performance:
- Without
key: Recreates all items on change (simple, works for small lists) - With
key: Reuses DOM nodes (efficient for large lists and reordering)
Render content outside parent DOM hierarchy.
Props:
mount?: string | HTMLElement- Target container (default: document.body)children: HTMLElement | (() => HTMLElement)- Content to portal
Examples:
Mount to body:
<Portal>
<Modal />
</Portal>Mount to specific container:
<Portal mount="#modal-root">
<Modal />
</Portal>Imperative:
const modal = Portal({
mount: document.getElementById('modal-root'),
children: () => {
const div = document.createElement('div')
div.className = 'modal'
return div
}
})Cleanup all active portals. Call when app unmounts.
import { cleanupPortals } from 'pulsar-ui'
// On app unmount
cleanupPortals()Display development warning (stripped in production).
Examples:
Simple:
warn('This is deprecated')With context:
warn({
message: 'Missing key prop',
component: 'For',
hint: 'Add a key function for better performance'
})Output:
[pulsar] [For] Missing key prop
Hint: Add a key function for better performance
Runtime assertion (only in development).
invariant(
value !== undefined,
'Value is required',
'MyComponent',
'Check your props'
)Throws:
[pulsar] [MyComponent] Value is required
Hint: Check your props
Tree-shakeable development flag.
if (DEV) {
console.log('Development mode')
traceComponentMount('MyComponent')
}
// Entire block removed in productionStart tracking a component (dev only).
traceComponentMount('TodoApp')
// [pulsar Trace] Mounted: TodoAppTrack component update.
traceComponentUpdate('TodoApp')
// [pulsar Trace] Updated: TodoApp (3 updates)Stop tracking and report stats.
traceComponentUnmount('TodoApp')
// [pulsar Trace] Unmounted: TodoApp
// Lifetime: 5432.50ms
// Updates: 15Warn about too many updates (default threshold: 100).
checkExcessiveUpdates('MyComponent', 50)
// Warns if component updated more than 50 timesState hook with signal-based reactivity.
const [count, setCount] = useState(0)
// Reading (subscribes to changes)
const value = count()
// Writing
setCount(5)
setCount(c => c + 1)Important: Must call count() to read value (not just count).
Effect hook that runs when dependencies change.
useEffect(() => {
console.log(`Count: ${count()}`)
// Cleanup function (optional)
return () => {
console.log('Cleanup')
}
}, [count])Without deps: Runs on every change to any signal read inside.
Memoized computation.
const expensiveValue = useMemo(() => {
return complexCalculation(count())
}, [count])
// Use it
const result = expensiveValue()Mutable ref object.
const inputRef = useRef<HTMLInputElement>()
useEffect(() => {
inputRef.current?.focus()
}, [])
return <input ref={inputRef} />Create a context.
const ThemeContext = createContext<'light' | 'dark'>('light')Provide context value.
<ThemeContext.Provider value={theme()}>
<App />
</ThemeContext.Provider>Consume context value.
const theme = useContext(ThemeContext)Bootstrap an pulsar app.
import { bootstrapApp } from 'pulsar-ui'
const app = bootstrapApp({
root: '#app',
component: App,
props: { message: 'Hello' }
})
app.mount()
// Later
app.unmount()interface IShowProps {
when: boolean | (() => boolean)
fallback?: HTMLElement | (() => HTMLElement)
children: HTMLElement | (() => HTMLElement)
}interface IForProps<T> {
each: T[] | (() => T[])
key?: (item: T, index: number) => string | number
children: (item: T, index: () => number) => HTMLElement
fallback?: HTMLElement | (() => HTMLElement)
}interface IPortalProps {
children: HTMLElement | (() => HTMLElement)
mount?: string | HTMLElement
}interface IDevWarning {
message: string
component?: string
hint?: string
}type BatchFn = () => void// ❌ Bad: 3 effect runs
setCount(1)
setName('Bob')
setAge(30)
// ✅ Good: 1 effect run
batch(() => {
setCount(1)
setName('Bob')
setAge(30)
})// ❌ Without key: recreates all on change
<For each={items()}>
{item => <Item data={item} />}
</For>
// ✅ With key: reuses DOM nodes
<For each={items()} key={item => item.id}>
{item => <Item data={item} />}
</For>// ❌ Coarse: updates even when only name changes
const [user, setUser] = useState({ name: 'Bob', age: 30 })
<div>{user().age}</div> // Re-renders when name changes too
// ✅ Fine: only updates when age changes
const [name, setName] = useState('Bob')
const [age, setAge] = useState(30)
<div>{age()}</div> // Only updates when age changes// ❌ Runs on every render
const filtered = items().filter(item => item.active)
// ✅ Only recomputes when items change
const filtered = useMemo(() => {
return items().filter(item => item.active)
}, [items])const Modal = ({ isOpen, onClose, children }) => {
return (
<Show when={isOpen()}>
<Portal mount="#modal-root">
<div class="modal-overlay" onClick={onClose}>
<div class="modal-content" onClick={e => e.stopPropagation()}>
{children}
</div>
</div>
</Portal>
</Show>
)
}const TodoList = () => {
const [todos, setTodos] = useState<Todo[]>([])
return (
<Show
when={() => todos().length > 0}
fallback={<EmptyState />}
>
<For each={todos} key={todo => todo.id}>
{(todo) => <TodoItem todo={todo} />}
</For>
</Show>
)
}const saveUser = async (user: User) => {
// Optimistic update
batch(() => {
setUser(user)
setStatus('saving')
})
try {
await api.saveUser(user)
setStatus('saved')
} catch (error) {
batch(() => {
setUser(previousUser)
setStatus('error')
setError(error.message)
})
}
}// React
const [count, setCount] = useState(0)
const value = count // value directly
// pulsar
const [count, setCount] = useState(0)
const value = count() // call to get value// React
useEffect(() => {
console.log(count)
}, [count])
// pulsar
useEffect(() => {
console.log(count()) // must call count()
}, [count])// React
{isVisible && <Component />}
// pulsar
<Show when={isVisible()}>
<Component />
</Show>// React
{items.map(item => <Item key={item.id} data={item} />)}
// pulsar
<For each={items()} key={item => item.id}>
{item => <Item data={item} />}
</For>// React
createPortal(<Modal />, document.body)
// pulsar
<Portal mount={document.body}>
<Modal />
</Portal>- Always call signals as functions:
count()notcount - Use batch() for multiple updates: Prevents excessive re-renders
- Provide keys in For components: For better performance
- Split coarse state: One signal per reactive value
- Use Show/For over imperative: More declarative and reactive
- Cleanup portals on unmount: Call
cleanupPortals() - Use dev utilities: Leverage warnings and invariants during development
- Check DEV flag: Wrap dev-only code in
if (DEV)
import {
useState,
useEffect,
batch,
Show,
For,
Portal
} from 'pulsar-ui'
interface Todo {
id: number
text: string
completed: boolean
}
const TodoApp = () => {
const [todos, setTodos] = useState<Todo[]>([])
const [filter, setFilter] = useState<'all' | 'active' | 'completed'>('all')
const [showModal, setShowModal] = useState(false)
const filteredTodos = useMemo(() => {
const list = todos()
if (filter() === 'all') return list
if (filter() === 'active') return list.filter(t => !t.completed)
return list.filter(t => t.completed)
}, [todos, filter])
const toggleTodo = (id: number) => {
setTodos(todos().map(todo =>
todo.id === id
? { ...todo, completed: !todo.completed }
: todo
))
}
const addTodo = (text: string) => {
batch(() => {
setTodos([...todos(), {
id: Date.now(),
text,
completed: false
}])
setShowModal(false)
})
}
return (
<div class="todo-app">
<header>
<h1>Todos</h1>
<button onClick={() => setShowModal(true)}>
Add Todo
</button>
</header>
<div class="filters">
<button onClick={() => setFilter('all')}>All</button>
<button onClick={() => setFilter('active')}>Active</button>
<button onClick={() => setFilter('completed')}>Completed</button>
</div>
<Show
when={() => filteredTodos().length > 0}
fallback={<p>No todos yet!</p>}
>
<For each={filteredTodos} key={todo => todo.id}>
{(todo) => (
<div
class={todo.completed ? 'completed' : ''}
onClick={() => toggleTodo(todo.id)}
>
{todo.text}
</div>
)}
</For>
</Show>
<Show when={showModal}>
<Portal mount="#modal-root">
<AddTodoModal
onAdd={addTodo}
onClose={() => setShowModal(false)}
/>
</Portal>
</Show>
</div>
)
}