React

React Hooks

Master React Hooks — useState, useEffect, useCallback, useMemo, useRef, and custom hooks.

useState

useState is the most fundamental hook. It adds state to functional components and returns the current value and a setter function.

'use client';

import { useState } from 'react';

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

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

// Object state
interface FormData {
  name: string;
  email: string;
  role: string;
}

function UserForm() {
  const [form, setForm] = useState<FormData>({
    name: '',
    email: '',
    role: 'developer',
  });

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

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

useEffect

useEffect handles side effects — data fetching, subscriptions, DOM manipulation, and timers. The cleanup function prevents memory leaks.

'use client';

import { useState, useEffect } from 'react';

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; // Prevent state update on unmount

    async function loadUser() {
      try {
        setLoading(true);
        const res = await fetch(`/api/users/${userId}`);
        const data = await res.json();

        if (!cancelled) {
          setUser(data);
          setError(null);
        }
      } catch (err) {
        if (!cancelled) {
          setError('Failed to load user');
        }
      } finally {
        if (!cancelled) setLoading(false);
      }
    }

    loadUser();

    // Cleanup: cancel if userId changes or component unmounts
    return () => { cancelled = true; };
  }, [userId]); // Re-run when userId changes

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error}</div>;
  if (!user) return null;

  return <div>{user.name}</div>;
}

useCallback & useMemo

These hooks optimize performance by memoizing functions and computed values, preventing unnecessary re-renders.

'use client';

import { useState, useCallback, useMemo } from 'react';

function ExpensiveComponent({ items, onSelect }: {
  items: string[];
  onSelect: (item: string) => void;
}) {
  console.log('ExpensiveComponent rendered');
  return (
    <ul>
      {items.map(item => (
        <li key={item} onClick={() => onSelect(item)}>{item}</li>
      ))}
    </ul>
  );
}

// Wrap with React.memo to prevent re-render unless props change
const MemoizedComponent = React.memo(ExpensiveComponent);

function Parent() {
  const [count, setCount] = useState(0);
  const [selected, setSelected] = useState('');
  const items = ['Apple', 'Banana', 'Cherry'];

  // useCallback: memoize function reference
  // Without this, new function ref on every render = MemoizedComponent re-renders
  const handleSelect = useCallback((item: string) => {
    setSelected(item);
  }, []); // No deps, same reference always

  // useMemo: memoize expensive computation
  const expensiveResult = useMemo(() => {
    console.log('Computing...');
    return items.filter(item => item.toLowerCase().includes('a'));
  }, [items]); // Only recompute when items changes

  return (
    <div>
      <button onClick={() => setCount(c => c + 1)}>Count: {count}</button>
      <p>Selected: {selected}</p>
      <MemoizedComponent items={expensiveResult} onSelect={handleSelect} />
    </div>
  );
}

Don't over-optimize. Only use useCallback and useMemo when you have a measured performance problem. Premature optimization adds complexity.

Custom Hooks

Custom hooks let you extract and reuse stateful logic across components. They always start with "use" and can call other hooks.

import { useState, useEffect } from 'react';

// Custom hook: persists state in localStorage
function useLocalStorage<T>(key: string, initialValue: T) {
  const [storedValue, setStoredValue] = useState<T>(() => {
    if (typeof window === 'undefined') return initialValue;

    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch {
      return initialValue;
    }
  });

  const setValue = (value: T | ((val: T) => T)) => {
    try {
      const valueToStore = value instanceof Function
        ? value(storedValue)
        : value;
      setStoredValue(valueToStore);
      window.localStorage.setItem(key, JSON.stringify(valueToStore));
    } catch (error) {
      console.error(error);
    }
  };

  return [storedValue, setValue] as const;
}

// Usage
function ThemeToggle() {
  const [theme, setTheme] = useLocalStorage<'light' | 'dark'>('theme', 'dark');

  return (
    <button onClick={() => setTheme(t => t === 'dark' ? 'light' : 'dark')}>
      Current theme: {theme}
    </button>
  );
}