Skip to content

Preact Advanced Hooks — useReducer, useRef, useMemo, useCallback Deep Dive

DodaTech Updated 2026-06-28 5 min read

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

  1. Using useMemo for trivial computations: Memoization adds overhead. Only use useMemo for operations that are provably expensive (thousands of operations).
  2. Omitting dependencies from arrays: useCallback(() => { doSomething(a, b) }, []) captures stale a and b. Include all dependencies.
  3. Using useRef when useState is needed: If changes to the value should update the UI, use useState not useRef.
  4. Creating useCallback wrappers unnecessarily: Passing inline arrow functions to HTML elements (not components) doesn't benefit from useCallback.
  5. Mutating state in useReducer: Always return new objects/arrays. state.todos.push(newTodo) mutates the previous state and prevents proper diffing.

Practice Questions

  1. When should you use useReducer instead of useState? Answer: When state has multiple sub-values, complex transition logic, or when the next state depends on the previous state in complex ways.

  2. What happens when you change ref.current? Answer: The component does NOT re-render. useRef is for mutable values that don't affect the visual output.

  3. How does useMemo differ from useCallback? Answer: useMemo returns the result of a function (a value). useCallback returns the function itself. useCallback(fn, deps) equals useMemo(() => fn, deps).

  4. Why would useCallback prevent child component re-renders? Answer: Combined with memo, 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

Does Preact support `memo`?

: Yes. Import memo from preact (not preact/hooks). It works identically to React's React.memo.

Can I use `useImperativeHandle` in Preact?

: Yes, combined with forwardRef from preact. It exposes custom methods to parent components via refs.

Does Preact have `useLayoutEffect`?

: Yes. In fact, Preact's useEffect runs synchronously like useLayoutEffect in React. Preact also provides useLayoutEffect as an alias.

Can I use multiple `useReducer` hooks in one component?

: Yes. Each useReducer call creates independent state. This is useful for managing unrelated state domains separately.

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