📌 useRef Hook

Access DOM & Store Mutable Values

What is useRef?

useRef returns a mutable object that persists across renders. It has two main uses: (1) accessing DOM elements directly, and (2) storing mutable values that don't trigger re-renders when changed.

useRef vs useState:

  • useState: Triggers re-render when updated
  • useRef: Does NOT trigger re-render when updated

📚 Basic Syntax

import { useRef } from 'react';

function Component() {
  const ref = useRef(initialValue);
  
  // Access/update value
  ref.current  // Read value
  ref.current = newValue  // Update (no re-render!)
  
  return <div>...</div>;
}

🎯 Use Case 1: DOM References

Focus Input on Mount:

function SearchBox() {
  const inputRef = useRef(null);
  
  useEffect(() => {
    // Focus input when component mounts
    inputRef.current.focus();
  }, []);
  
  return (
    <input 
      ref={inputRef}
      type="text"
      placeholder="Search..."
    />
  );
}

Scroll to Element:

function ScrollExample() {
  const topRef = useRef(null);
  const bottomRef = useRef(null);
  
  const scrollToTop = () => {
    topRef.current.scrollIntoView({ behavior: 'smooth' });
  };
  
  const scrollToBottom = () => {
    bottomRef.current.scrollIntoView({ behavior: 'smooth' });
  };
  
  return (
    <div>
      <div ref={topRef}>Top of page</div>
      
      {/* Long content */}
      
      <div ref={bottomRef}>Bottom of page</div>
      
      <button onClick={scrollToTop}>↑ Top</button>
      <button onClick={scrollToBottom}>↓ Bottom</button>
    </div>
  );
}

Play/Pause Video:

function VideoPlayer({ src }) {
  const videoRef = useRef(null);
  const [isPlaying, setIsPlaying] = useState(false);
  
  const togglePlay = () => {
    if (isPlaying) {
      videoRef.current.pause();
    } else {
      videoRef.current.play();
    }
    setIsPlaying(!isPlaying);
  };
  
  return (
    <div>
      <video ref={videoRef} src={src} />
      <button onClick={togglePlay}>
        {isPlaying ? '⏸ Pause' : '▶ Play'}
      </button>
    </div>
  );
}

Measure Element Size:

function MeasureBox() {
  const boxRef = useRef(null);
  const [dimensions, setDimensions] = useState({});
  
  useEffect(() => {
    if (boxRef.current) {
      const { width, height } = boxRef.current.getBoundingClientRect();
      setDimensions({ width, height });
    }
  }, []);
  
  return (
    <div>
      <div ref={boxRef} style={{ width: '200px', height: '100px', background: 'blue' }}>
        Box
      </div>
      <p>Width: {dimensions.width}px</p>
      <p>Height: {dimensions.height}px</p>
    </div>
  );
}

💾 Use Case 2: Storing Mutable Values

Previous Value:

function Counter() {
  const [count, setCount] = useState(0);
  const prevCountRef = useRef();
  
  useEffect(() => {
    prevCountRef.current = count;
  }, [count]);
  
  const prevCount = prevCountRef.current;
  
  return (
    <div>
      <p>Current: {count}</p>
      <p>Previous: {prevCount}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}

// Custom hook version
function usePrevious(value) {
  const ref = useRef();
  
  useEffect(() => {
    ref.current = value;
  }, [value]);
  
  return ref.current;
}

// Usage
const prevCount = usePrevious(count);

Interval ID:

function Timer() {
  const [seconds, setSeconds] = useState(0);
  const [isRunning, setIsRunning] = useState(false);
  const intervalRef = useRef(null);
  
  const start = () => {
    if (intervalRef.current !== null) return;
    
    setIsRunning(true);
    intervalRef.current = setInterval(() => {
      setSeconds(s => s + 1);
    }, 1000);
  };
  
  const stop = () => {
    if (intervalRef.current === null) return;
    
    clearInterval(intervalRef.current);
    intervalRef.current = null;
    setIsRunning(false);
  };
  
  const reset = () => {
    stop();
    setSeconds(0);
  };
  
  // Cleanup on unmount
  useEffect(() => {
    return () => {
      if (intervalRef.current) {
        clearInterval(intervalRef.current);
      }
    };
  }, []);
  
  return (
    <div>
      <h2>{seconds}s</h2>
      <button onClick={start} disabled={isRunning}>Start</button>
      <button onClick={stop} disabled={!isRunning}>Stop</button>
      <button onClick={reset}>Reset</button>
    </div>
  );
}

Render Count:

function RenderCounter() {
  const renderCount = useRef(0);
  
  // Increment on every render (doesn't cause re-render!)
  renderCount.current += 1;
  
  return <div>Renders: {renderCount.current}</div>;
}

Latest Value in Callback:

function ChatRoom({ roomId }) {
  const [messages, setMessages] = useState([]);
  const latestMessagesRef = useRef(messages);
  
  // Keep ref in sync
  useEffect(() => {
    latestMessagesRef.current = messages;
  }, [messages]);
  
  useEffect(() => {
    const ws = new WebSocket(`wss://api.com/room/${roomId}`);
    
    ws.onmessage = (event) => {
      const newMessage = JSON.parse(event.data);
      // Use ref to get latest messages without recreating listener
      setMessages([...latestMessagesRef.current, newMessage]);
    };
    
    return () => ws.close();
  }, [roomId]); // No need to include messages in dependencies!
  
  return <div>{/* render messages */}</div>;
}

🎨 Complete Example: Click Outside

function Dropdown() {
  const [isOpen, setIsOpen] = useState(false);
  const dropdownRef = useRef(null);
  
  useEffect(() => {
    function handleClickOutside(event) {
      if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
        setIsOpen(false);
      }
    }
    
    // Add listener when dropdown is open
    if (isOpen) {
      document.addEventListener('mousedown', handleClickOutside);
    }
    
    return () => {
      document.removeEventListener('mousedown', handleClickOutside);
    };
  }, [isOpen]);
  
  return (
    <div ref={dropdownRef} className="dropdown">
      <button onClick={() => setIsOpen(!isOpen)}>
        Menu {isOpen ? '▲' : '▼'}
      </button>
      
      {isOpen && (
        <div className="dropdown-menu">
          <a href="#">Profile</a>
          <a href="#">Settings</a>
          <a href="#">Logout</a>
        </div>
      )}
    </div>
  );
}

// Reusable custom hook
function useClickOutside(callback) {
  const ref = useRef(null);
  
  useEffect(() => {
    function handleClick(event) {
      if (ref.current && !ref.current.contains(event.target)) {
        callback();
      }
    }
    
    document.addEventListener('mousedown', handleClick);
    return () => document.removeEventListener('mousedown', handleClick);
  }, [callback]);
  
  return ref;
}

// Usage
function Modal({ onClose }) {
  const modalRef = useClickOutside(onClose);
  
  return (
    <div ref={modalRef} className="modal">
      Modal content
    </div>
  );
}

🔄 useRef with useEffect

// Debounce with useRef
function SearchBox() {
  const [search, setSearch] = useState('');
  const [results, setResults] = useState([]);
  const timeoutRef = useRef(null);
  
  useEffect(() => {
    // Clear previous timeout
    if (timeoutRef.current) {
      clearTimeout(timeoutRef.current);
    }
    
    // Set new timeout
    timeoutRef.current = setTimeout(() => {
      if (search) {
        fetch(`/api/search?q=${search}`)
          .then(res => res.json())
          .then(setResults);
      }
    }, 500);
    
    return () => {
      if (timeoutRef.current) {
        clearTimeout(timeoutRef.current);
      }
    };
  }, [search]);
  
  return (
    <div>
      <input 
        value={search}
        onChange={e => setSearch(e.target.value)}
        placeholder="Search..."
      />
      {results.map(r => <div key={r.id}>{r.name}</div>)}
    </div>
  );
}

⚠️ Common Mistakes

1. Using Ref Value in Render

// ❌ Won't update UI when ref changes
function Bad() {
  const ref = useRef(0);
  return <div>{ref.current}</div>;  // Won't re-render
}

// ✅ Use state if you need re-renders
function Good() {
  const [count, setCount] = useState(0);
  return <div>{count}</div>;
}

2. Ref Not Set Yet

// ❌ Ref might be null
function Bad() {
  const ref = useRef(null);
  ref.current.focus();  // Error if ref.current is null!
}

// ✅ Check if ref is set
function Good() {
  const ref = useRef(null);
  
  useEffect(() => {
    if (ref.current) {
      ref.current.focus();
    }
  }, []);
}

🎯 Key Takeaways