More Hooks

useContext, useRef, useMemo, useCallback and custom hooks

Beyond useState and useEffect

React provides several other built-in hooks for specific use cases. These hooks help you manage context, references, memoization, and more. You can also create your own custom hooks!

useRef

Creates a mutable reference that persists across renders without causing re-renders:

import { useRef, useEffect } from 'react';

// Accessing DOM elements
function FocusInput() {
  const inputRef = useRef(null);

  const handleClick = () => {
    inputRef.current.focus();
  };

  return (
    <div>
      <input ref={inputRef} type="text" />
      <button onClick={handleClick}>Focus Input</button>
    </div>
  );
}

// Storing mutable values (doesn't trigger re-render)
function Timer() {
  const [count, setCount] = useState(0);
  const intervalRef = useRef(null);

  const start = () => {
    intervalRef.current = setInterval(() => {
      setCount(c => c + 1);
    }, 1000);
  };

  const stop = () => {
    clearInterval(intervalRef.current);
  };

  return (
    <div>
      <p>{count}</p>
      <button onClick={start}>Start</button>
      <button onClick={stop}>Stop</button>
    </div>
  );
}

// Previous value pattern
function Counter() {
  const [count, setCount] = useState(0);
  const prevCountRef = useRef();

  useEffect(() => {
    prevCountRef.current = count;
  });

  return (
    <p>
      Current: {count}, Previous: {prevCountRef.current}
    </p>
  );
}

useContext

Access context values without prop drilling:

import { createContext, useContext, useState } from 'react';

// 1. Create context
const ThemeContext = createContext('light');

// 2. Provider component
function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');

  const toggleTheme = () => {
    setTheme(t => t === 'light' ? 'dark' : 'light');
  };

  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

// 3. Use context in any child component
function ThemedButton() {
  const { theme, toggleTheme } = useContext(ThemeContext);

  return (
    <button
      onClick={toggleTheme}
      style={{ 
        background: theme === 'dark' ? '#333' : '#fff',
        color: theme === 'dark' ? '#fff' : '#333'
      }}
    >
      Toggle Theme ({theme})
    </button>
  );
}

// 4. Wrap app with provider
function App() {
  return (
    <ThemeProvider>
      <Header />
      <ThemedButton />
      <Footer />
    </ThemeProvider>
  );
}

useMemo

Memoize expensive calculations to avoid re-computing on every render:

import { useMemo, useState } from 'react';

function ExpensiveList({ items, filter }) {
  // Only recalculates when items or filter changes
  const filteredItems = useMemo(() => {
    console.log('Filtering items...');
    return items.filter(item => 
      item.name.toLowerCase().includes(filter.toLowerCase())
    );
  }, [items, filter]);

  return (
    <ul>
      {filteredItems.map(item => (
        <li key={item.id}>{item.name}</li>
      ))}
    </ul>
  );
}

// Complex calculation example
function Fibonacci({ n }) {
  const result = useMemo(() => {
    const fib = (num) => {
      if (num <= 1) return num;
      return fib(num - 1) + fib(num - 2);
    };
    return fib(n);
  }, [n]);

  return <p>Fibonacci({n}) = {result}</p>;
}

⚠️ Don't Overuse

Only use useMemo for truly expensive calculations. Premature optimization can make code harder to read without real benefits.

useCallback

Memoize functions to maintain referential equality across renders:

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

// Child component wrapped in memo
const ExpensiveChild = memo(function ExpensiveChild({ onClick }) {
  console.log('Child rendered');
  return <button onClick={onClick}>Click me</button>;
});

function Parent() {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('');

  // Without useCallback: new function on every render
  // const handleClick = () => console.log('Clicked!');

  // With useCallback: same function reference
  const handleClick = useCallback(() => {
    console.log('Clicked!');
  }, []);  // Empty deps = never changes

  // With dependencies
  const handleSubmit = useCallback(() => {
    console.log('Submitting:', name);
  }, [name]);  // New function only when name changes

  return (
    <div>
      <input value={name} onChange={e => setName(e.target.value)} />
      <button onClick={() => setCount(c => c + 1)}>
        Count: {count}
      </button>
      <ExpensiveChild onClick={handleClick} />
    </div>
  );
}

useReducer

Alternative to useState for complex state logic:

import { useReducer } from 'react';

// Reducer function
function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 };
    case 'decrement':
      return { count: state.count - 1 };
    case 'reset':
      return { count: 0 };
    case 'set':
      return { count: action.payload };
    default:
      throw new Error('Unknown action');
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, { count: 0 });

  return (
    <div>
      <p>Count: {state.count}</p>
      <button onClick={() => dispatch({ type: 'increment' })}>+</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
      <button onClick={() => dispatch({ type: 'reset' })}>Reset</button>
      <button onClick={() => dispatch({ type: 'set', payload: 100 })}>
        Set to 100
      </button>
    </div>
  );
}

Custom Hooks

Extract reusable logic into custom hooks:

// Custom hook for local storage
function useLocalStorage(key, initialValue) {
  const [value, setValue] = useState(() => {
    const saved = localStorage.getItem(key);
    return saved ? JSON.parse(saved) : initialValue;
  });

  useEffect(() => {
    localStorage.setItem(key, JSON.stringify(value));
  }, [key, value]);

  return [value, setValue];
}

// Usage
function Settings() {
  const [theme, setTheme] = useLocalStorage('theme', 'light');
  return <button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}>{theme}</button>;
}

// Custom hook for fetch
function useFetch(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    setLoading(true);
    fetch(url)
      .then(res => res.json())
      .then(setData)
      .catch(setError)
      .finally(() => setLoading(false));
  }, [url]);

  return { data, loading, error };
}

// Usage
function UserProfile({ id }) {
  const { data: user, loading, error } = useFetch(`/api/users/${id}`);
  
  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error!</p>;
  return <h1>{user.name}</h1>;
}

// Custom hook for window size
function useWindowSize() {
  const [size, setSize] = useState({
    width: window.innerWidth,
    height: window.innerHeight
  });

  useEffect(() => {
    const handleResize = () => {
      setSize({ width: window.innerWidth, height: window.innerHeight });
    };
    window.addEventListener('resize', handleResize);
    return () => window.removeEventListener('resize', handleResize);
  }, []);

  return size;
}

// Usage
function ResponsiveComponent() {
  const { width } = useWindowSize();
  return <p>Width: {width}px</p>;
}

Hooks Rules

Rules of Hooks

  • 1. Only call hooks at the top level (not inside loops, conditions, or nested functions)
  • 2. Only call hooks from React functions (components or custom hooks)
  • 3. Custom hooks must start with "use" (useLocalStorage, useFetch)

🎯 Hooks Best Practices

  • ✓ Use useRef for DOM access and mutable values that don't need re-renders
  • ✓ Use useContext to avoid prop drilling
  • ✓ Use useMemo for expensive calculations only
  • ✓ Use useCallback when passing callbacks to memoized children
  • ✓ Use useReducer for complex state with multiple actions
  • ✓ Extract shared logic into custom hooks
  • ✓ Name custom hooks starting with "use"