Skip to main content
Hooks are a powerful feature in Preact that let you use state and other features without writing a class component. Preact’s hooks implementation is fully compatible with React Hooks.

What are Hooks?

Hooks are functions that let you “hook into” Preact state and lifecycle features from function components. They make it easier to reuse stateful logic between components and organize your code.

Available Hooks

Preact provides all the standard hooks from the preact/hooks package:
  • useState - Manage local state
  • useEffect - Perform side effects
  • useReducer - Alternative to useState for complex state
  • useRef - Create a mutable ref object
  • useMemo - Memoize expensive computations
  • useCallback - Memoize callback functions
  • useContext - Access context values
  • useLayoutEffect - Run effects synchronously after DOM mutations
  • useImperativeHandle - Customize ref exposure
  • useDebugValue - Display custom hook labels in DevTools
  • useErrorBoundary - Catch errors in components
  • useId - Generate unique IDs for accessibility

useState

The useState hook lets you add state to function components. It returns an array with the current state value and a function to update it.
import { useState } from 'preact/hooks';

function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>
        Increment
      </button>
    </div>
  );
}

Functional Updates

When the new state depends on the previous state, pass a function to the setter:
function Counter() {
  const [count, setCount] = useState(0);

  const increment = () => {
    // Good: Uses functional update
    setCount(prevCount => prevCount + 1);
  };

  return <button onClick={increment}>Count: {count}</button>;
}

useEffect

The useEffect hook lets you perform side effects in function components. Effects run after the browser paints, making them non-blocking.
1
Basic Effect
2
import { useState, useEffect } from 'preact/hooks';

function Timer() {
  const [seconds, setSeconds] = useState(0);

  useEffect(() => {
    const interval = setInterval(() => {
      setSeconds(s => s + 1);
    }, 1000);

    // Cleanup function
    return () => clearInterval(interval);
  }, []); // Empty array = run once on mount

  return <div>Seconds: {seconds}</div>;
}
3
Effect Dependencies
4
The second argument to useEffect is a dependency array. The effect re-runs when dependencies change:
5
function UserData({ userId }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    fetch(`/api/users/${userId}`)
      .then(res => res.json())
      .then(setUser);
  }, [userId]); // Re-run when userId changes

  return user ? <div>{user.name}</div> : <div>Loading...</div>;
}
6
Cleanup
7
Return a cleanup function from effects to clean up subscriptions, timers, etc:
8
useEffect(() => {
  const subscription = api.subscribe(data => {
    console.log(data);
  });

  return () => {
    subscription.unsubscribe();
  };
}, []);

useReducer

For complex state logic, useReducer is often preferable to useState. It’s built on top of useState internally.
import { useReducer } from 'preact/hooks';

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 };
    case 'decrement':
      return { count: state.count - 1 };
    case 'reset':
      return { count: 0 };
    default:
      throw new Error('Unknown action type');
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, { count: 0 });

  return (
    <div>
      <p>Count: {state.count}</p>
      <button onClick={() => dispatch({ type: 'increment' })}>
        +
      </button>
      <button onClick={() => dispatch({ type: 'decrement' })}>
        -
      </button>
      <button onClick={() => dispatch({ type: 'reset' })}>
        Reset
      </button>
    </div>
  );
}

Lazy Initialization

useReducer accepts an optional third argument for lazy initialization:
function init(initialCount) {
  return { count: initialCount };
}

function Counter({ initialCount }) {
  const [state, dispatch] = useReducer(
    reducer,
    initialCount,
    init  // Initialization function
  );
  
  // ...
}

Rules of Hooks

Hooks have two important rules that must be followed:
Only call hooks at the top level of your function. Don’t call hooks inside loops, conditions, or nested functions.
// Bad: Hook inside condition
function Component({ condition }) {
  if (condition) {
    const [state, setState] = useState(0); // Don't do this!
  }
}

// Good: Hook at top level
function Component({ condition }) {
  const [state, setState] = useState(0);
  
  if (condition) {
    // Use state here
  }
}

Custom Hooks

Custom hooks let you extract component logic into reusable functions. They’re just functions that use other hooks.
import { useState, useEffect } from 'preact/hooks';

function useLocalStorage(key, initialValue) {
  // Get from local storage then parse stored json or return initialValue
  const [storedValue, setStoredValue] = useState(() => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      console.log(error);
      return initialValue;
    }
  });

  // Return a wrapped version of useState's setter function that
  // persists the new value to localStorage.
  const setValue = value => {
    try {
      const valueToStore =
        value instanceof Function ? value(storedValue) : value;
      setStoredValue(valueToStore);
      window.localStorage.setItem(key, JSON.stringify(valueToStore));
    } catch (error) {
      console.log(error);
    }
  };

  return [storedValue, setValue];
}

// Usage
function Component() {
  const [name, setName] = useLocalStorage('name', 'Bob');
  
  return (
    <input
      value={name}
      onChange={e => setName(e.target.value)}
    />
  );
}

Best Practices

1
Use descriptive state variable names
2
Choose names that clearly indicate what the state represents:
3
// Good
const [isLoading, setIsLoading] = useState(false);
const [userData, setUserData] = useState(null);

// Less clear
const [flag, setFlag] = useState(false);
const [data, setData] = useState(null);
4
Keep effects focused
5
Each effect should handle one concern. Split complex effects into multiple smaller ones:
6
// Good: Separate effects for separate concerns
function Component({ userId }) {
  useEffect(() => {
    // Handle user data fetching
  }, [userId]);

  useEffect(() => {
    // Handle analytics tracking
  }, []);
}

// Less ideal: One effect doing too much
function Component({ userId }) {
  useEffect(() => {
    // Fetching, tracking, and more...
  }, [userId]);
}
7
Extract reusable logic
8
When you find yourself repeating the same hook patterns, extract them into custom hooks:
9
// Before: Repeated pattern
function ComponentA() {
  const [data, setData] = useState(null);
  useEffect(() => {
    fetch('/api/a').then(r => r.json()).then(setData);
  }, []);
}

function ComponentB() {
  const [data, setData] = useState(null);
  useEffect(() => {
    fetch('/api/b').then(r => r.json()).then(setData);
  }, []);
}

// After: Custom hook
function useFetch(url) {
  const [data, setData] = useState(null);
  useEffect(() => {
    fetch(url).then(r => r.json()).then(setData);
  }, [url]);
  return data;
}

function ComponentA() {
  const data = useFetch('/api/a');
}

function ComponentB() {
  const data = useFetch('/api/b');
}

Implementation Details

Preact’s hooks are implemented in hooks/src/index.js and integrate deeply with Preact’s rendering system. Key implementation details:
  • Hooks use an internal currentComponent variable to track which component is rendering
  • Each hook call increments a currentIndex counter to maintain hook state between renders
  • useState is implemented using useReducer internally
  • Effects are queued and flushed after painting using requestAnimationFrame
  • Hook state is stored in component.__hooks._list array
Reference: hooks/src/index.js:172-175

Next Steps

Context API

Learn how to share state across components using Context

Refs

Access DOM elements and persist values with refs

Build docs developers (and LLMs) love