Skip to content

Hooks Reference

The framework exposes Preact hooks for state and lifecycle management. These work exactly like React hooks.

All hooks are available on the global fullfinity object:

const {
useState,
useEffect,
useRef,
useCallback,
useMemo,
useStore, // Custom: access stores
useSlot, // Custom: access slots with auto re-render
useLifecycle, // Custom: lifecycle callbacks
} = window.fullfinity;

Manage local component state.

registry.component('Counter', function() {
const { useState, html } = window.fullfinity;
// Basic state
const [count, setCount] = useState(0);
// Object state
const [form, setForm] = useState({ name: '', email: '' });
// Lazy initial state (computed once)
const [data, setData] = useState(() => computeExpensiveValue());
return html`
<div>
<p>Count: ${count}</p>
<button onClick=${() => setCount(c => c + 1)}>Increment</button>
<input
value=${form.name}
onInput=${e => setForm(f => ({ ...f, name: e.target.value }))}
/>
</div>
`;
});
// Direct value
setCount(5);
// Functional update (use when new state depends on old state)
setCount(prevCount => prevCount + 1);
// Object state - always spread to preserve other fields
setForm(prev => ({ ...prev, name: 'New Name' }));
// Array state
setItems(prev => [...prev, newItem]); // Add
setItems(prev => prev.filter(i => i.id !== id)); // Remove
setItems(prev => prev.map(i => i.id === id ? { ...i, ...updates } : i)); // Update

Handle side effects (data fetching, subscriptions, DOM manipulation).

registry.component('UserProfile', function({ userId }) {
const { useState, useEffect, html } = window.fullfinity;
const [user, setUser] = useState(null);
// Runs after every render
useEffect(() => {
document.title = user?.name || 'Loading...';
});
// Runs once on mount (empty dependency array)
useEffect(() => {
console.log('Component mounted');
return () => console.log('Component unmounted');
}, []);
// Runs when userId changes
useEffect(() => {
let cancelled = false;
async function fetchUser() {
const response = await fetch(`/api/users/${userId}`);
const data = await response.json();
if (!cancelled) {
setUser(data);
}
}
fetchUser();
// Cleanup function
return () => {
cancelled = true;
};
}, [userId]);
return html`<div>${user?.name || 'Loading...'}</div>`;
});
Dependency ArrayWhen Effect Runs
Not providedAfter every render
[]Once on mount
[a, b]When a or b changes

Return a cleanup function to run when:

  • Component unmounts
  • Before effect re-runs (when dependencies change)
useEffect(() => {
const subscription = api.subscribe(handler);
// Cleanup
return () => {
subscription.unsubscribe();
};
}, []);

Create a mutable reference that persists across renders.

registry.component('AutoFocusInput', function() {
const { useRef, useEffect, html } = window.fullfinity;
const inputRef = useRef(null);
// Focus input on mount
useEffect(() => {
inputRef.current?.focus();
}, []);
return html`<input ref=${inputRef} type="text" />`;
});
// DOM element reference
const divRef = useRef(null);
// Previous value
const prevValueRef = useRef(value);
useEffect(() => {
prevValueRef.current = value;
});
// Instance variable (doesn't trigger re-render)
const timerRef = useRef(null);
function startTimer() {
timerRef.current = setInterval(() => {}, 1000);
}
function stopTimer() {
clearInterval(timerRef.current);
}
// Tracking mount status
const mountedRef = useRef(true);
useEffect(() => {
return () => { mountedRef.current = false; };
}, []);

Memoize a callback function.

registry.component('ItemList', function({ items, onSelect }) {
const { useCallback, html } = window.fullfinity;
// Without useCallback, handleClick is recreated every render
// With useCallback, it's only recreated when dependencies change
const handleClick = useCallback((item) => {
onSelect(item);
}, [onSelect]);
return html`
<ul>
${items.map(item => html`
<li key=${item.id} onClick=${() => handleClick(item)}>
${item.name}
</li>
`)}
</ul>
`;
});
  • When passing callbacks to optimized child components
  • When callback is a dependency of useEffect
  • When callback is computationally expensive to create

Memoize an expensive computation.

registry.component('DataTable', function({ data, filter }) {
const { useMemo, html } = window.fullfinity;
// Only recompute when data or filter changes
const filteredData = useMemo(() => {
return data.filter(item =>
item.name.toLowerCase().includes(filter.toLowerCase())
);
}, [data, filter]);
// Expensive sorting
const sortedData = useMemo(() => {
return [...filteredData].sort((a, b) => a.date - b.date);
}, [filteredData]);
return html`
<table>
${sortedData.map(item => html`
<tr key=${item.id}>
<td>${item.name}</td>
<td>${item.date}</td>
</tr>
`)}
</table>
`;
});
  • Expensive calculations
  • Referential equality matters (e.g., dependency arrays)
  • Derived data that shouldn’t recompute on every render

Custom hook to access framework stores (see Stores & Events).

registry.component('CartSummary', function() {
const { useStore, html } = window.fullfinity;
// Access the cart store
const cart = useStore('cart');
if (!cart) {
return html`<div>Loading...</div>`;
}
return html`
<div class="cart-summary">
<p>Items: ${cart.items.length}</p>
<p>Total: $${cart.total.toFixed(2)}</p>
</div>
`;
});

Custom hook to access extension slot contents. Automatically re-renders when addons register into the slot.

registry.component('ProductCard', function(props) {
const { useSlot, html } = window.fullfinity;
// Get slot components - re-renders when slot changes
const afterPrice = useSlot('ProductCard:afterPrice');
const beforeTitle = useSlot('ProductCard:beforeTitle');
return html`
<div class="product-card">
${beforeTitle.map(C => html`<${C} ...${props} />`)}
<h3>${props.name}</h3>
<span class="price">${props.price}</span>
${afterPrice.map(C => html`<${C} ...${props} />`)}
</div>
`;
});
MethodRe-renders on slot changeUse case
useSlot(name)YesComponents with extension points
registry.getSlot(name)NoOne-time reads, outside components

Custom hook for lifecycle callbacks. Provides a cleaner API for common lifecycle patterns.

registry.component('AnalyticsWidget', function({ pageId }) {
const { useLifecycle, html } = window.fullfinity;
useLifecycle({
onMounted: () => {
console.log('Widget mounted');
analytics.track('widget_view', { pageId });
},
onWillUnmount: () => {
console.log('Widget unmounting');
analytics.track('widget_close', { pageId });
},
onUpdated: () => {
console.log('Widget updated');
}
});
return html`<div class="widget">...</div>`;
});
CallbackWhen it runs
onMountedAfter first render (component appears in DOM)
onUpdatedAfter each re-render (not first mount)
onWillUnmountBefore component is removed from DOM
// Using useEffect (standard Preact)
useEffect(() => {
console.log('Mounted');
return () => console.log('Unmounting');
}, []);
// Using useLifecycle (cleaner for multiple callbacks)
useLifecycle({
onMounted: () => console.log('Mounted'),
onWillUnmount: () => console.log('Unmounting'),
onUpdated: () => console.log('Updated') // Bonus: separate update callback
});

The framework also exposes Preact Signals for reactive state:

const { signal, computed, effect } = window.fullfinity;
// Create a signal
const count = signal(0);
// Read value
console.log(count.value); // 0
// Write value
count.value = 5;
// Computed signal
const doubled = computed(() => count.value * 2);
// Effect (runs when dependencies change)
effect(() => {
console.log('Count is now:', count.value);
});

Signals are useful for:

  • Global state without stores
  • Fine-grained reactivity
  • Performance optimization

Create reusable hooks by combining built-in hooks:

// useFetch - reusable data fetching
function useFetch(url) {
const { useState, useEffect } = window.fullfinity;
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let cancelled = false;
async function fetchData() {
try {
setLoading(true);
const response = await fetch(url);
if (!response.ok) throw new Error('Fetch failed');
const json = await response.json();
if (!cancelled) setData(json);
} catch (e) {
if (!cancelled) setError(e);
} finally {
if (!cancelled) setLoading(false);
}
}
fetchData();
return () => { cancelled = true; };
}, [url]);
return { data, loading, error };
}
// useLocalStorage - persist state to localStorage
function useLocalStorage(key, initialValue) {
const { useState, useEffect } = window.fullfinity;
const [value, setValue] = useState(() => {
const stored = localStorage.getItem(key);
return stored ? JSON.parse(stored) : initialValue;
});
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue];
}
// useDebounce - debounce a value
function useDebounce(value, delay) {
const { useState, useEffect } = window.fullfinity;
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const timer = setTimeout(() => setDebouncedValue(value), delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debouncedValue;
}
// Usage
registry.component('SearchResults', function() {
const { useState, html } = window.fullfinity;
const [query, setQuery] = useState('');
const debouncedQuery = useDebounce(query, 300);
const { data, loading } = useFetch(`/api/search?q=${debouncedQuery}`);
return html`
<input
value=${query}
onInput=${e => setQuery(e.target.value)}
placeholder="Search..."
/>
${loading ? html`<p>Searching...</p>` : html`
<ul>
${data?.results?.map(r => html`<li>${r.title}</li>`)}
</ul>
`}
`;
});
  1. Only call hooks at the top level - Don’t call in loops, conditions, or nested functions
// Bad
if (condition) {
const [value, setValue] = useState(0);
}
// Good
const [value, setValue] = useState(0);
if (condition) {
// use value here
}
  1. Only call hooks in component functions
// Bad
const [value, setValue] = useState(0); // Outside component
// Good
registry.component('MyComponent', function() {
const [value, setValue] = useState(0); // Inside component
});
  1. Hooks order must be consistent
// Bad - order changes between renders
if (condition) {
useState(0);
}
useState('');
// Good - always same order
useState(0);
useState('');
// Use condition inside render instead

Common gotchas with dependency arrays:

// Missing dependency (stale closure)
useEffect(() => {
fetchData(userId); // userId might be stale
}, []); // Should include userId
// Too many dependencies
const handleClick = useCallback(() => {
doSomething(a, b, c, d, e, f);
}, [a, b, c, d, e, f]); // Consider refactoring
// Object/array dependencies (referential equality)
const config = { key: 'value' };
useEffect(() => {
// This runs every render because config is new object each time
}, [config]);
// Solution: useMemo the config
const config = useMemo(() => ({ key: 'value' }), []);
  1. Avoid creating objects/functions in render
// Bad - new object every render
<Component style={{ color: 'red' }} />
// Good - stable reference
const style = useMemo(() => ({ color: 'red' }), []);
<Component style=${style} />
  1. Use functional updates
// Bad - might use stale state
setCount(count + 1);
// Good - always latest state
setCount(c => c + 1);
  1. Memoize expensive components
// For child components that receive the same props
const MemoizedChild = useMemo(() => html`
<${ExpensiveComponent} data=${data} />
`, [data]);