Hooks Reference
The framework exposes Preact hooks for state and lifecycle management. These work exactly like React hooks.
Available Hooks
Section titled “Available 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;useState
Section titled “useState”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> `;});Updating State
Section titled “Updating State”// Direct valuesetCount(5);
// Functional update (use when new state depends on old state)setCount(prevCount => prevCount + 1);
// Object state - always spread to preserve other fieldssetForm(prev => ({ ...prev, name: 'New Name' }));
// Array statesetItems(prev => [...prev, newItem]); // AddsetItems(prev => prev.filter(i => i.id !== id)); // RemovesetItems(prev => prev.map(i => i.id === id ? { ...i, ...updates } : i)); // UpdateuseEffect
Section titled “useEffect”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 Array Rules
Section titled “Dependency Array Rules”| Dependency Array | When Effect Runs |
|---|---|
| Not provided | After every render |
[] | Once on mount |
[a, b] | When a or b changes |
Cleanup Function
Section titled “Cleanup Function”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(); };}, []);useRef
Section titled “useRef”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" />`;});Common Use Cases
Section titled “Common Use Cases”// DOM element referenceconst divRef = useRef(null);
// Previous valueconst 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 statusconst mountedRef = useRef(true);useEffect(() => { return () => { mountedRef.current = false; };}, []);useCallback
Section titled “useCallback”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 to Use
Section titled “When to Use”- When passing callbacks to optimized child components
- When callback is a dependency of useEffect
- When callback is computationally expensive to create
useMemo
Section titled “useMemo”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> `;});When to Use
Section titled “When to Use”- Expensive calculations
- Referential equality matters (e.g., dependency arrays)
- Derived data that shouldn’t recompute on every render
useStore
Section titled “useStore”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> `;});useSlot
Section titled “useSlot”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> `;});useSlot vs registry.getSlot
Section titled “useSlot vs registry.getSlot”| Method | Re-renders on slot change | Use case |
|---|---|---|
useSlot(name) | Yes | Components with extension points |
registry.getSlot(name) | No | One-time reads, outside components |
useLifecycle
Section titled “useLifecycle”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>`;});Lifecycle Callbacks
Section titled “Lifecycle Callbacks”| Callback | When it runs |
|---|---|
onMounted | After first render (component appears in DOM) |
onUpdated | After each re-render (not first mount) |
onWillUnmount | Before component is removed from DOM |
Comparison with useEffect
Section titled “Comparison with useEffect”// 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});Signals (Advanced)
Section titled “Signals (Advanced)”The framework also exposes Preact Signals for reactive state:
const { signal, computed, effect } = window.fullfinity;
// Create a signalconst count = signal(0);
// Read valueconsole.log(count.value); // 0
// Write valuecount.value = 5;
// Computed signalconst 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
Custom Hooks
Section titled “Custom Hooks”Create reusable hooks by combining built-in hooks:
// useFetch - reusable data fetchingfunction 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 localStoragefunction 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 valuefunction 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;}
// Usageregistry.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> `} `;});Rules of Hooks
Section titled “Rules of Hooks”- Only call hooks at the top level - Don’t call in loops, conditions, or nested functions
// Badif (condition) { const [value, setValue] = useState(0);}
// Goodconst [value, setValue] = useState(0);if (condition) { // use value here}- Only call hooks in component functions
// Badconst [value, setValue] = useState(0); // Outside component
// Goodregistry.component('MyComponent', function() { const [value, setValue] = useState(0); // Inside component});- Hooks order must be consistent
// Bad - order changes between rendersif (condition) { useState(0);}useState('');
// Good - always same orderuseState(0);useState('');// Use condition inside render insteadHook Dependencies
Section titled “Hook Dependencies”Common gotchas with dependency arrays:
// Missing dependency (stale closure)useEffect(() => { fetchData(userId); // userId might be stale}, []); // Should include userId
// Too many dependenciesconst 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 configconst config = useMemo(() => ({ key: 'value' }), []);Performance Tips
Section titled “Performance Tips”- Avoid creating objects/functions in render
// Bad - new object every render<Component style={{ color: 'red' }} />
// Good - stable referenceconst style = useMemo(() => ({ color: 'red' }), []);<Component style=${style} />- Use functional updates
// Bad - might use stale statesetCount(count + 1);
// Good - always latest statesetCount(c => c + 1);- Memoize expensive components
// For child components that receive the same propsconst MemoizedChild = useMemo(() => html` <${ExpensiveComponent} data=${data} />`, [data]);