Preact Advanced Hooks — useReducer, useRef, useMemo, useCallback Deep Dive
In this tutorial, you will learn about Preact Advanced Hooks. We cover key concepts, practical examples, and best practices to help you master this topic.
Learn advanced Preact hooks: useReducer for complex state, useRef for DOM access, useMemo for memoization, and useCallback for stable function references in the 3kB framework.
In this lesson, you'll dive deeper into the advanced hooks available in preact/hooks and learn when and why to use each one.
What You'll Learn
How to use useReducer for complex state logic, useRef for DOM references and mutable values, useMemo for expensive computations, and useCallback for stable function references.
Why It Matters
These hooks solve specific problems: complex state transitions with useReducer, DOM access and mutable refs with useRef, and performance optimization with useMemo and useCallback.
flowchart LR
A[Component Renders] --> B{Need complex state?}
B -->|Yes| C[useReducer]
B -->|No| D[useState]
A --> E{Need DOM access?}
E -->|Yes| F[useRef]
A --> G{Expensive computation?}
G -->|Yes| H[useMemo]
A --> I{Stable callback?}
I -->|Yes| J[useCallback]
style C fill:#673ab8,color:#fff
style F fill:#673ab8,color:#fff
style H fill:#673ab8,color:#fff
style J fill:#673ab8,color:#fff
useReducer Deep Dive
For state logic that involves multiple sub-values or complex transitions:
import { useReducer } from 'preact/hooks';
const initialState = {
todos: [],
filter: 'all',
nextId: 1
};
function todoReducer(state, action) {
switch (action.type) {
case 'add':
return {
...state,
todos: [...state.todos, { id: state.nextId, text: action.payload, done: false }],
nextId: state.nextId + 1
};
case 'toggle':
return {
...state,
todos: state.todos.map(t =>
t.id === action.payload ? { ...t, done: !t.done } : t
)
};
case 'delete':
return {
...state,
todos: state.todos.filter(t => t.id !== action.payload)
};
case 'setFilter':
return { ...state, filter: action.payload };
default:
return state;
}
}
Output: The reducer handles four actions: add, toggle, delete, and setFilter. Each action returns a new state immutably.
useRef for DOM Access
Access and manipulate DOM elements directly:
import { useRef, useEffect } from 'preact/hooks';
function Canvas() {
const canvasRef = useRef(null);
useEffect(() => {
const canvas = canvasRef.current;
const ctx = canvas.getContext('2d');
// Draw a simple shape
ctx.fillStyle = '#673ab8';
ctx.fillRect(10, 10, 100, 100);
ctx.fillStyle = '#fff';
ctx.font = '16px sans-serif';
ctx.fillText('Preact', 25, 70);
}, []);
return <canvas ref={canvasRef} width={200} height={200} />;
}
Output: A 200x200 canvas with a purple rectangle and "Preact" text. The ref provides direct access to the canvas DOM element for imperative drawing.
useRef for Mutable Values
Store values that persist across renders without causing re-renders:
import { useRef, useState, useEffect } from 'preact/hooks';
function Stopwatch() {
const [time, setTime] = useState(0);
const intervalRef = useRef(null);
const startTimeRef = useRef(null);
const start = () => {
startTimeRef.current = Date.now() - time;
intervalRef.current = setInterval(() => {
setTime(Date.now() - startTimeRef.current);
}, 10);
};
const stop = () => {
clearInterval(intervalRef.current);
intervalRef.current = null;
};
const reset = () => {
stop();
setTime(0);
};
useEffect(() => {
return () => clearInterval(intervalRef.current); // Cleanup
}, []);
return (
<div>
<p>{(time / 1000).toFixed(2)}s</p>
<button onClick={start}>Start</button>
<button onClick={stop}>Stop</button>
<button onClick={reset}>Reset</button>
</div>
);
}
Output: A precision stopwatch. intervalRef stores the interval ID without causing re-renders when it changes. startTimeRef stores the base timestamp for accurate elapsed time calculation.
useMemo for Expensive Computations
Avoid re-computing values that haven't changed:
import { useMemo, useState } from 'preact/hooks';
function PrimeCalculator({ limit }) {
const [color, setColor] = useState('#673ab8');
// Expensive computation — only re-runs when limit changes
const primes = useMemo(() => {
console.log('Calculating primes up to', limit);
const result = [];
for (let i = 2; i <= limit; i++) {
let isPrime = true;
for (let j = 2; j <= Math.sqrt(i); j++) {
if (i % j === 0) { isPrime = false; break; }
}
if (isPrime) result.push(i);
}
return result;
}, [limit]);
return (
<div>
<p style={{ color }}>Primes up to {limit}: {primes.join(', ')}</p>
<button onClick={() => setColor('#ff5722')}>Change Color</button>
</div>
);
}
Output: Changing the color doesn't re-trigger the prime calculation. Only changing limit re-computes the primes array, saving CPU time.
useCallback for Stable Functions
Maintain function identity across renders to prevent child re-renders:
import { useCallback, useState, memo } from 'preact/hooks';
const ExpensiveChild = memo(function ExpensiveChild({ onAction }) {
console.log('Child rendered');
return <button onClick={onAction}>Click</button>;
});
function Parent() {
const [count, setCount] = useState(0);
// Stable reference — doesn't change unless count changes
const handleClick = useCallback(() => {
console.log('Action triggered, count:', count);
}, [count]);
// Without useCallback, a new function is created every render
// causing ExpensiveChild to re-render every time
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(c => c + 1)}>Increment</button>
<ExpensiveChild onAction={handleClick} />
</div>
);
}
Output: The ExpensiveChild only re-renders when count changes (which changes handleClick identity), not when the parent re-renders for other reasons.
Common Mistakes
- Using
useMemofor trivial computations: Memoization adds overhead. Only useuseMemofor operations that are provably expensive (thousands of operations). - Omitting dependencies from arrays:
useCallback(() => { doSomething(a, b) }, [])captures staleaandb. Include all dependencies. - Using
useRefwhenuseStateis needed: If changes to the value should update the UI, useuseStatenotuseRef. - Creating
useCallbackwrappers unnecessarily: Passing inline arrow functions to HTML elements (not components) doesn't benefit fromuseCallback. - Mutating state in
useReducer: Always return new objects/arrays.state.todos.push(newTodo)mutates the previous state and prevents proper diffing.
Practice Questions
When should you use
useReducerinstead ofuseState? Answer: When state has multiple sub-values, complex transition logic, or when the next state depends on the previous state in complex ways.What happens when you change
ref.current? Answer: The component does NOT re-render.useRefis for mutable values that don't affect the visual output.How does
useMemodiffer fromuseCallback? Answer:useMemoreturns the result of a function (a value).useCallbackreturns the function itself.useCallback(fn, deps)equalsuseMemo(() => fn, deps).Why would
useCallbackprevent child component re-renders? Answer: Combined withmemo, the child uses referential equality to skip re-rendering. A stable callback reference means the child's props didn't change.
Challenge
Build a custom useDebounce hook using useRef and useEffect that delays updating a value until after a specified delay. Use it to debounce a search input.
Mini Project
Create a data visualization component that uses useMemo to compute chart data from raw input, useRef to access a canvas for drawing, and useCallback to expose stable event handlers.
FAQ
What's Next
Learn about Preact Signals to understand Preact's reactive state management with signals for fine-grained reactivity.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro