Follow AiTechWorlds on LinkedIn for professional AI content!Follow Now →

React Hooks Explained: From useState to useEffect and Beyond

React hooks tutorial covering useState, useEffect, useContext, useRef, useMemo, useCallback, and custom hooks — with real examples for every hook.

A
AiTechWorlds Team
May 27, 2026 8 min read
📱

Get more content like this on Telegram!

Daily AI tips, notes & resources — free

Join Free →

React Hooks Explained: From useState to useEffect and Beyond

When React Hooks landed in 2019, I spent a week convinced they were making things more complicated. Then I tried to add a data-fetching pattern to a class component for the first time in months and remembered exactly why hooks existed.

Hooks are simpler than class components once you understand the mental model. They let you think about your component in terms of what it needs to do, not when in the lifecycle it should do it.

This guide covers every hook you'll actually use in real React applications, with honest notes about the gotchas.


useState — Managing Component State

You already know useState. Let me show the patterns you might not.

// Basic
const [count, setCount] = useState(0);

// Object state — never mutate directly
const [user, setUser] = useState({ name: '', email: '' });
const updateName = (name: string) => setUser(prev => ({ ...prev, name }));

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

// Lazy initialization — function runs only on first render
const [data, setData] = useState(() => {
  const saved = localStorage.getItem('data');
  return saved ? JSON.parse(saved) : [];
});

The functional update form (prev => prev + 1) is important when you're updating state inside event handlers or effects. It avoids stale closure issues where count might be out of date.


useEffect — Side Effects

useEffect is where most React confusion comes from. Here's the mental model that cleared it up for me: effects run after render, not during.

// Runs after every render — usually not what you want
useEffect(() => {
  document.title = `${count} tasks`;
});

// Runs once after mount — fetch initial data, subscriptions
useEffect(() => {
  fetchUser().then(setUser);
}, []);

// Runs when count changes
useEffect(() => {
  document.title = `${count} tasks`;
}, [count]);

// Cleanup — runs before the next effect and on unmount
useEffect(() => {
  const interval = setInterval(() => setTick(t => t + 1), 1000);
  return () => clearInterval(interval);  // Cleanup
}, []);

The Data Fetching Pattern

function UserProfile({ userId }: { userId: number }) {
  const [user, setUser] = useState<User | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);
  
  useEffect(() => {
    let cancelled = false;  // Prevents state updates after unmount
    
    setLoading(true);
    setError(null);
    
    fetch(`/api/users/${userId}`)
      .then(res => {
        if (!res.ok) throw new Error('Failed to fetch');
        return res.json();
      })
      .then(data => {
        if (!cancelled) setUser(data);
      })
      .catch(err => {
        if (!cancelled) setError(err.message);
      })
      .finally(() => {
        if (!cancelled) setLoading(false);
      });
    
    return () => { cancelled = true; };
  }, [userId]);
  
  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error: {error}</p>;
  if (!user) return null;
  
  return <div>{user.name}</div>;
}

The cancelled flag is important — it prevents setting state on an unmounted component (which React warns about).


useContext — Sharing State Across Components

// 1. Create context
const ThemeContext = createContext<{
  theme: 'light' | 'dark';
  toggleTheme: () => void;
} | null>(null);

// 2. Create provider
function ThemeProvider({ children }: { children: React.ReactNode }) {
  const [theme, setTheme] = useState<'light' | 'dark'>('dark');
  
  const toggleTheme = () => setTheme(t => t === 'light' ? 'dark' : 'light');
  
  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

// 3. Custom hook for safe consumption
function useTheme() {
  const context = useContext(ThemeContext);
  if (!context) throw new Error('useTheme must be used inside ThemeProvider');
  return context;
}

// 4. Use anywhere inside the tree
function DarkModeToggle() {
  const { theme, toggleTheme } = useTheme();
  return (
    <button onClick={toggleTheme}>
      {theme === 'dark' ? '☀️ Light' : '🌙 Dark'}
    </button>
  );
}

useRef — DOM Access and Mutable Values

useRef has two distinct use cases:

// Use case 1: DOM element access
function AutoFocusInput() {
  const inputRef = useRef<HTMLInputElement>(null);
  
  useEffect(() => {
    inputRef.current?.focus();  // Focus on mount
  }, []);
  
  return <input ref={inputRef} placeholder="Auto-focused" />;
}

// Use case 2: Mutable value that doesn't trigger re-render
function StopWatch() {
  const [time, setTime] = useState(0);
  const intervalRef = useRef<number | null>(null);
  
  function start() {
    intervalRef.current = setInterval(() => setTime(t => t + 1), 1000);
  }
  
  function stop() {
    if (intervalRef.current) clearInterval(intervalRef.current);
  }
  
  return (
    <div>
      <p>{time}s</p>
      <button onClick={start}>Start</button>
      <button onClick={stop}>Stop</button>
    </div>
  );
}

The key: changing ref.current does not trigger a re-render. Use refs for values you need to persist across renders but don't need in the rendered output.


useReducer — Complex State Logic

When state has multiple related pieces that change together, useReducer is cleaner than multiple useState calls:

type State = {
  items: Item[];
  loading: boolean;
  error: string | null;
};

type Action =
  | { type: 'FETCH_START' }
  | { type: 'FETCH_SUCCESS'; payload: Item[] }
  | { type: 'FETCH_ERROR'; payload: string }
  | { type: 'DELETE_ITEM'; payload: number };

function reducer(state: State, action: Action): State {
  switch (action.type) {
    case 'FETCH_START':
      return { ...state, loading: true, error: null };
    case 'FETCH_SUCCESS':
      return { ...state, loading: false, items: action.payload };
    case 'FETCH_ERROR':
      return { ...state, loading: false, error: action.payload };
    case 'DELETE_ITEM':
      return { ...state, items: state.items.filter(i => i.id !== action.payload) };
    default:
      return state;
  }
}

function ItemList() {
  const [state, dispatch] = useReducer(reducer, { items: [], loading: false, error: null });
  
  useEffect(() => {
    dispatch({ type: 'FETCH_START' });
    fetchItems()
      .then(items => dispatch({ type: 'FETCH_SUCCESS', payload: items }))
      .catch(err => dispatch({ type: 'FETCH_ERROR', payload: err.message }));
  }, []);
  
  // ...
}

useMemo and useCallback — Performance

Use these sparingly — only when you've measured a performance problem.

// useMemo — cache expensive calculation
const sortedItems = useMemo(() => {
  return [...items].sort((a, b) => a.name.localeCompare(b.name));
}, [items]);  // Only re-sorts when items changes

// useCallback — stable function reference for child components
const handleDelete = useCallback((id: number) => {
  setItems(prev => prev.filter(item => item.id !== id));
}, []);  // Stable reference — doesn't change on every render

One thing I learned the hard way: wrapping everything in useMemo and useCallback doesn't make your app faster. Each one has its own overhead. Profile first, optimize second.


Custom Hooks — Reusable Logic

The real power of hooks: extracting logic into reusable functions.

// useLocalStorage — persists state to localStorage
function useLocalStorage<T>(key: string, initialValue: T) {
  const [value, setValue] = useState<T>(() => {
    try {
      const item = localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch {
      return initialValue;
    }
  });
  
  const setStoredValue = (newValue: T) => {
    setValue(newValue);
    localStorage.setItem(key, JSON.stringify(newValue));
  };
  
  return [value, setStoredValue] as const;
}

// Usage
const [theme, setTheme] = useLocalStorage('theme', 'dark');
// useDebounce — debounce a value
function useDebounce<T>(value: T, delay: number): T {
  const [debounced, setDebounced] = useState(value);
  
  useEffect(() => {
    const handler = setTimeout(() => setDebounced(value), delay);
    return () => clearTimeout(handler);
  }, [value, delay]);
  
  return debounced;
}

// Usage — search that doesn't fire on every keystroke
const [query, setQuery] = useState('');
const debouncedQuery = useDebounce(query, 300);

useEffect(() => {
  if (debouncedQuery) fetchResults(debouncedQuery);
}, [debouncedQuery]);

Custom hooks are just functions. Any logic that uses hooks and can be reused is a candidate for extraction.


Hooks Quick Reference

HookPurposeKey gotcha
useStateComponent stateUse functional updates when new state depends on old
useEffectSide effectsDon't forget cleanup; include all dependencies
useContextConsume contextTriggers re-render on any context value change
useRefDOM refs, mutable valuesChanging ref.current doesn't trigger re-render
useReducerComplex state logicGood replacement for multiple related useState
useMemoCache expensive valuesOnly use when measured perf problem exists
useCallbackStable function refsRequired for memoized children

For building on top of hooks in a complete app, our React tutorial for beginners shows them in context. When your state management needs grow beyond hooks, our React state management 2025 guide covers Zustand and Jotai. And for React performance optimization beyond useMemo/useCallback, see our React performance guide.


Frequently Asked Questions

What are React Hooks?

Functions that let you use state and lifecycle features in functional components. Introduced in React 16.8, they replaced class components as the standard.

When should I use useMemo vs useCallback?

useMemo caches computed values. useCallback caches function references. Use both only when you have a measured performance problem.

What is the useEffect dependency array?

Controls when the effect runs. Empty: once on mount. No array: every render. [a, b]: when a or b changes.

What is a custom React Hook?

A function starting with 'use' that uses other hooks. Lets you extract and reuse stateful logic across components.

Share this article:

Frequently Asked Questions

React Hooks are functions that let you use state and other React features in functional components. Introduced in React 16.8 (2019), they replaced class components as the standard way to write React. They make it easier to reuse stateful logic (custom hooks), keep related code together, and avoid the confusing 'this' binding in class components.
A

AiTechWorlds Team

✓ Verified Writer

The AiTechWorlds team is passionate about AI, technology, and education. We create high-quality, research-backed content to help you learn, grow, and succeed in the modern digital world.

Related Articles

10K+ Members Growing Daily

Get Free AI Notes Daily

Join AiTechWorlds on Telegram and get daily AI tips, prompt engineering templates, coding resources, and exclusive content — 100% free!

📚 Free Study Notes🤖 AI Tips Daily⚡ Prompt Templates💻 Coding Resources
Join Free Channel

No spam. Leave anytime.

!