Books/React & Next.js Essentials/React Hooks Essentials

    React Hooks Essentials

    React Hooks Essentials

    Hooks are special functions that let you "hook into" React features from functional components. They're the mechanism behind state management, side effects, and most of the dynamic behavior in modern React apps.

    The Rules of Hooks

    Before diving in, there are two rules you need to know:

    1. Only call hooks at the top level — not inside loops, conditions, or nested functions
    2. Only call hooks in React components or custom hooks — not in regular functions
    // ✅ Correct — hooks at the top level
    function MyComponent() {
      const [count, setCount] = useState(0);
      const [name, setName] = useState("");
    
      return <div>{count}</div>;
    }
    
    // ❌ Wrong — hook inside a condition
    function MyComponent({ isLoggedIn }: { isLoggedIn: boolean }) {
      if (isLoggedIn) {
        const [user, setUser] = useState(null); // Don't do this!
      }
      return <div>...</div>;
    }

    useState — Managing Data

    You already learned useState in the previous tutorial. Here's a quick recap with additional patterns:

    import { useState } from "react";
    
    function Form() {
      // Simple state
      const [email, setEmail] = useState("");
    
      // State with explicit type
      const [error, setError] = useState<string | null>(null);
    
      // State based on previous value (use callback form)
      const [count, setCount] = useState(0);
      const increment = () => setCount(prev => prev + 1);
    
      // Object state
      const [form, setForm] = useState({ name: "", email: "", message: "" });
      const updateField = (field: string, value: string) => {
        setForm(prev => ({ ...prev, [field]: value }));
      };
    }

    Pro tip: When the new state depends on the old state, use the callback form: setState(prev => newValue). This avoids bugs with stale closures.

    useEffect — Side Effects

    useEffect runs code after your component renders. It's used for anything that interacts with the outside world — API calls, timers, event listeners, and more.

    Basic Pattern

    import { useState, useEffect } from "react";
    
    function UserProfile({ userId }: { userId: string }) {
      const [user, setUser] = useState<User | null>(null);
      const [loading, setLoading] = useState(true);
    
      useEffect(() => {
        // This code runs after the component renders
        const fetchUser = async () => {
          try {
            const response = await fetch(`/api/users/${userId}`);
            const data = await response.json();
            setUser(data);
          } catch (error) {
            console.error("Failed to fetch user:", error);
          } finally {
            setLoading(false);
          }
        };
    
        fetchUser();
      }, [userId]); // <-- dependency array
    
      if (loading) return <p>Loading...</p>;
      if (!user) return <p>User not found</p>;
    
      return <h1>{user.name}</h1>;
    }

    The Dependency Array

    The second argument to useEffect controls when the effect runs:

    // Runs after EVERY render
    useEffect(() => {
      console.log("Rendered!");
    });
    
    // Runs ONCE after the first render (like componentDidMount)
    useEffect(() => {
      console.log("Component mounted!");
    }, []);
    
    // Runs when 'userId' or 'page' changes
    useEffect(() => {
      fetchData(userId, page);
    }, [userId, page]);
    Dependency ArrayWhen It Runs
    Not providedEvery render
    [] (empty)Once on mount
    [a, b]When a or b changes

    Cleanup Function

    Some effects need to clean up after themselves — like removing event listeners or canceling timers:

    useEffect(() => {
      // Set up
      const handleResize = () => {
        setWidth(window.innerWidth);
      };
      window.addEventListener("resize", handleResize);
    
      // Clean up (runs before the effect re-runs or when component unmounts)
      return () => {
        window.removeEventListener("resize", handleResize);
      };
    }, []);

    The cleanup function returned from useEffect runs:

    • Before the effect re-runs (if dependencies changed)
    • When the component is removed from the page (unmounts)

    What to ask your AI: "Create a useEffect that fetches data from [endpoint] when [dependency] changes. Include loading and error states."

    useRef — Accessing DOM Elements and Persistent Values

    useRef creates a mutable reference that persists across renders without causing re-renders when changed.

    Accessing DOM Elements

    import { useRef, useEffect } from "react";
    
    function SearchInput() {
      const inputRef = useRef<HTMLInputElement>(null);
    
      useEffect(() => {
        // Focus the input when the component mounts
        inputRef.current?.focus();
      }, []);
    
      return <input ref={inputRef} placeholder="Search..." />;
    }

    Storing Mutable Values

    function Timer() {
      const [seconds, setSeconds] = useState(0);
      const intervalRef = useRef<NodeJS.Timeout | null>(null);
    
      const start = () => {
        intervalRef.current = setInterval(() => {
          setSeconds(prev => prev + 1);
        }, 1000);
      };
    
      const stop = () => {
        if (intervalRef.current) {
          clearInterval(intervalRef.current);
        }
      };
    
      // Clean up on unmount
      useEffect(() => {
        return () => {
          if (intervalRef.current) clearInterval(intervalRef.current);
        };
      }, []);
    
      return (
        <div>
          <p>{seconds} seconds</p>
          <button onClick={start}>Start</button>
          <button onClick={stop}>Stop</button>
        </div>
      );
    }

    Key difference from state: changing a ref does not cause a re-render.

    Custom Hooks — Reusable Logic

    Custom hooks let you extract component logic into reusable functions. They're regular functions that use hooks inside:

    useFetch — A Custom Data Fetching Hook

    function useFetch<T>(url: string) {
      const [data, setData] = useState<T | null>(null);
      const [loading, setLoading] = useState(true);
      const [error, setError] = useState<string | null>(null);
    
      useEffect(() => {
        const fetchData = async () => {
          try {
            setLoading(true);
            const response = await fetch(url);
            if (!response.ok) throw new Error("Failed to fetch");
            const result = await response.json();
            setData(result);
          } catch (err) {
            setError(err instanceof Error ? err.message : "An error occurred");
          } finally {
            setLoading(false);
          }
        };
    
        fetchData();
      }, [url]);
    
      return { data, loading, error };
    }
    
    // Usage — clean and reusable!
    function UserList() {
      const { data: users, loading, error } = useFetch<User[]>("/api/users");
    
      if (loading) return <p>Loading...</p>;
      if (error) return <p>Error: {error}</p>;
    
      return (
        <ul>
          {users?.map(user => (
            <li key={user.id}>{user.name}</li>
          ))}
        </ul>
      );
    }

    useLocalStorage — Persistent State

    function useLocalStorage<T>(key: string, initialValue: T) {
      const [value, setValue] = useState<T>(() => {
        try {
          const stored = localStorage.getItem(key);
          return stored ? JSON.parse(stored) : initialValue;
        } catch {
          return initialValue;
        }
      });
    
      useEffect(() => {
        localStorage.setItem(key, JSON.stringify(value));
      }, [key, value]);
    
      return [value, setValue] as const;
    }
    
    // Usage
    function Settings() {
      const [theme, setTheme] = useLocalStorage("theme", "light");
    
      return (
        <button onClick={() => setTheme(theme === "light" ? "dark" : "light")}>
          Current: {theme}
        </button>
      );
    }

    useDebounce — Delayed Updates

    function useDebounce<T>(value: T, delay: number): T {
      const [debouncedValue, setDebouncedValue] = useState(value);
    
      useEffect(() => {
        const timer = setTimeout(() => setDebouncedValue(value), delay);
        return () => clearTimeout(timer);
      }, [value, delay]);
    
      return debouncedValue;
    }
    
    // Usage — search only fires after user stops typing
    function SearchBar() {
      const [query, setQuery] = useState("");
      const debouncedQuery = useDebounce(query, 300);
    
      useEffect(() => {
        if (debouncedQuery) {
          // Fetch search results
          fetch(`/api/search?q=${debouncedQuery}`);
        }
      }, [debouncedQuery]);
    
      return (
        <input
          value={query}
          onChange={(e) => setQuery(e.target.value)}
          placeholder="Search..."
        />
      );
    }

    What to ask your AI: "Create a custom hook called use[Name] that [does this]. It should return [these values]."

    Common Patterns Summary

    HookPurposeWhen to Use
    useStateManage component dataAny data that changes and affects UI
    useEffectSide effectsAPI calls, subscriptions, DOM updates
    useRefDOM access / mutable valuesFocus inputs, store intervals, previous values
    Custom hooksReusable logicExtract repeated patterns across components

    What's Next?

    You now have a solid foundation in React's core hooks. The next tutorial covers Next.js Routing and Pages — how to build multi-page applications with file-based routing.

    What to ask your AI: "I'm building a [feature] and I need to [track/fetch/listen to] something. Which hook should I use and how?"


    🌐 www.genai-mentor.ai