TUTORIAL

React Hooks Deep Dive: useState, useEffect, and Custom Hooks

Go beyond the basics of React hooks. Learn the rules, pitfalls, and patterns for useState, useEffect, and building reusable custom hooks.


Table of Contents

React Hooks Deep Dive: useState, useEffect, and Custom Hooks

Hooks transformed React by letting you use state and lifecycle features in function components. Understanding them deeply — including their pitfalls — separates good React code from great React code.

Rules of Hooks

Break these rules and React will behave unpredictably: 1. Only call hooks at the **top level** — never inside loops, conditions, or nested functions. 2. Only call hooks from **React function components** or other hooks.

useState in Depth

TSX
import { useState } from 'react';

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

  // Functional update — always use when new state depends on old state
  const increment = () => setCount(prev => prev + 1);

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

Object State Pattern

TSX
interface FormState {
  name: string;
  email: string;
  loading: boolean;
}

function Form() {
  const [form, setForm] = useState<FormState>({
    name: '',
    email: '',
    loading: false,
  });

  const updateField = (field: keyof FormState) =>
    (e: React.ChangeEvent<HTMLInputElement>) =>
      setForm(prev => ({ ...prev, [field]: e.target.value }));

  return (
    <form>
      <input value={form.name} onChange={updateField('name')} />
      <input value={form.email} onChange={updateField('email')} />
    </form>
  );
}

useEffect Mastery

useEffect runs after every render by default. The dependency array controls when:

TSX
useEffect(() => {
  // Runs after every render
});

useEffect(() => {
  // Runs once on mount
}, []);

useEffect(() => {
  // Runs when userId changes
  fetchUser(userId);
}, [userId]);

Cleanup Functions

Always return a cleanup function for subscriptions, timers, and event listeners:

TSX
useEffect(() => {
  const controller = new AbortController();

  fetch(`/api/users/${userId}`, { signal: controller.signal })
    .then(r => r.json())
    .then(setUser)
    .catch(e => {
      if (e.name !== 'AbortError') setError(e.message);
    });

  return () => controller.abort(); // cleanup on unmount or re-run
}, [userId]);

Performance Hooks

useCallback

Memoizes a function reference to prevent unnecessary re-renders of children:

TSX
const handleSubmit = useCallback(
  async (data: FormData) => {
    await api.submit(data);
    onSuccess();
  },
  [onSuccess] // recreate only when onSuccess changes
);

useMemo

Memoizes an expensive computed value:

TSX
const sortedItems = useMemo(
  () => items.slice().sort((a, b) => a.name.localeCompare(b.name)),
  [items] // only re-sort when items changes
);
Don't over-memoize. `useMemo` and `useCallback` have their own overhead. Reach for them when you have a measured performance problem, not pre-emptively.

Building Custom Hooks

Custom hooks let you extract and reuse stateful logic:

TSX
// hooks/useFetch.ts
import { useState, useEffect } from 'react';

interface FetchState<T> {
  data: T | null;
  loading: boolean;
  error: string | null;
}

export function useFetch<T>(url: string): FetchState<T> {
  const [state, setState] = useState<FetchState<T>>({
    data: null,
    loading: true,
    error: null,
  });

  useEffect(() => {
    const controller = new AbortController();
    setState(s => ({ ...s, loading: true }));

    fetch(url, { signal: controller.signal })
      .then(r => {
        if (!r.ok) throw new Error(`HTTP ${r.status}`);
        return r.json() as Promise<T>;
      })
      .then(data => setState({ data, loading: false, error: null }))
      .catch(e => {
        if (e.name !== 'AbortError') {
          setState({ data: null, loading: false, error: e.message });
        }
      });

    return () => controller.abort();
  }, [url]);

  return state;
}

Usage:

TSX
function UserProfile({ userId }: { userId: string }) {
  const { data: user, loading, error } = useFetch<User>(`/api/users/${userId}`);

  if (loading) return <Spinner />;
  if (error) return <ErrorMsg message={error} />;
  if (!user) return null;
  return <div>{user.name}</div>;
}

Custom hooks are one of the most powerful patterns in React. Extract any stateful logic that's used in more than one component into a custom hook.


Was this article helpful?

w

webencher Editorial

Software engineers and technical writers with 10+ years of combined experience in algorithms, systems design, and web development. Every article is reviewed for accuracy, depth, and practical applicability.

More by this author →