React Hooks Complete Guide: useState, useEffect, useCallback, useRef, useMemo

Master React Hooks with this comprehensive guide. Learn useState, useEffect, useCallback, useRef, and useMemo with practical examples and best practices for building modern React applications.

React Hooks Complete Guide: useState, useEffect, useCallback, useRef, useMemo

React Hooks revolutionized how we write React components, allowing us to use state and other React features without writing classes. In this comprehensive guide, we’ll dive deep into five essential hooks: useState, useEffect, useCallback, useRef, and useMemo. Understanding these hooks will help you write cleaner, more efficient React applications.

What are React Hooks?

Hooks are functions that let you “hook into” React state and lifecycle features from function components. Introduced in React 16.8, they’ve become the standard way to write React components.

“Hooks solve exactly known problems we’ve had building apps with React.” — React Documentation

useState: Managing Component State

The useState hook is the foundation of state management in functional components. It allows you to add state to your components with minimal boilerplate.

Basic Syntax

import { useState } from 'react';

const [state, setState] = useState(initialValue);

Practical Example

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

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      
      <input 
        value={name} 
        onChange={(e) => setName(e.target.value)} 
        placeholder="Enter your name"
      />
      
      <button onClick={() => setTodos([...todos, `Task ${todos.length + 1}`])}>
        Add Todo
      </button>
    </div>
  );
}

Best Practices

1. Use functional updates for state that depends on previous state:

// Not recommended
setCount(count + 1);

// Recommended
setCount(prevCount => prevCount + 1);

2. Split state when values change independently:

// Avoid combining unrelated state
const [user, setUser] = useState({ name: '', age: 0, email: '' });

// Better: separate state for independent values
const [name, setName] = useState('');
const [age, setAge] = useState(0);
const [email, setEmail] = useState('');

3. Initialize state carefully:

// Expensive computation runs on every render
const [value, setValue] = useState(expensiveComputation());

// Lazy initialization - computation runs only once
const [value, setValue] = useState(() => expensiveComputation());

useEffect: Handling Side Effects

The useEffect hook lets you perform side effects in function components. It’s your go-to hook for data fetching, subscriptions, DOM manipulation, and more.

Basic Syntax

import { useEffect } from 'react';

useEffect(() => {
  // Side effect logic
  return () => {
    // Cleanup (optional)
  };
}, [dependencies]);

Common Use Cases

1. Data Fetching

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    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]);

  if (loading) return <div>Loading...</div>;
  return <div>{user?.name}</div>;
}

2. Event Listeners

function WindowTracker() {
  const [windowSize, setWindowSize] = useState({
    width: window.innerWidth,
    height: window.innerHeight
  });

  useEffect(() => {
    const handleResize = () => {
      setWindowSize({
        width: window.innerWidth,
        height: window.innerHeight
      });
    };

    window.addEventListener('resize', handleResize);
    
    // Cleanup on unmount
    return () => window.removeEventListener('resize', handleResize);
  }, []);

  return <div>Window: {windowSize.width} x {windowSize.height}</div>;
}

3. Subscriptions

function ChatRoom({ roomId }) {
  useEffect(() => {
    const connection = createConnection(roomId);
    connection.connect();
    
    return () => connection.disconnect();
  }, [roomId]);

  return <ChatWindow />;
}

Understanding Dependency Arrays

// Runs on every render
useEffect(() => {
  console.log('No dependencies');
});

// Runs only once (on mount)
useEffect(() => {
  console.log('Empty dependency array');
}, []);

// Runs when specific values change
useEffect(() => {
  console.log('Dependency changed:', count);
}, [count]);

useCallback: Optimizing Function References

The useCallback hook returns a memoized version of a callback function. It’s essential for preventing unnecessary re-renders of child components that rely on reference equality.

Basic Syntax

import { useCallback } from 'react';

const memoizedCallback = useCallback(() => {
  doSomething(a, b);
}, [a, b]);

When to Use useCallback

1. Passing callbacks to optimized child components:

function Parent() {
  const [count, setCount] = useState(0);
  const [other, setOther] = useState(0);

  // Without useCallback, this function is recreated on every render
  const handleClick = useCallback(() => {
    console.log('Count:', count);
  }, [count]);

  return (
    <>
      <MemoizedButton onClick={handleClick} />
      <button onClick={() => setOther(other + 1)}>Other: {other}</button>
    </>
  );
}

2. Dependencies in useEffect:

function SearchComponent({ searchQuery }) {
  const handleSearch = useCallback((query) => {
    console.log('Searching:', query);
  }, []);

  useEffect(() => {
    handleSearch(searchQuery);
  }, [handleSearch, searchQuery]);

  return <SearchInput onSearch={handleSearch} />;
}

Common Mistake: Overusing useCallback

// Don't wrap everything in useCallback
const handleClick = useCallback(() => {
  doSomething();
}, []);

// Only use when needed for optimization
const handleClick = () => {
  doSomething();
};

useRef: Accessing DOM and Persisting Values

The useRef hook serves two main purposes: accessing DOM elements directly and storing mutable values that persist across renders without causing re-renders.

Basic Syntax

import { useRef } from 'react';

const refContainer = useRef(initialValue);

Use Cases

1. Accessing DOM Elements

function FocusInput() {
  const inputRef = useRef(null);

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

  return (
    <>
      <input ref={inputRef} type="text" placeholder="Click button to focus" />
      <button onClick={focusInput}>Focus Input</button>
    </>
  );
}

2. Storing Mutable Values

function Timer() {
  const [count, setCount] = useState(0);
  const intervalRef = useRef(null);

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

    return () => clearInterval(intervalRef.current);
  }, []);

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

  return (
    <div>
      <p>Timer: {count}s</p>
      <button onClick={stopTimer}>Stop</button>
    </div>
  );
}

3. Tracking Previous Values

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

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

  const prevCount = prevCountRef.current;

  return (
    <div>
      <p>Current: {count}, Previous: {prevCount}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}

useMemo: Expensive Computation Optimization

The useMemo hook memoizes the result of a computation, recalculating only when dependencies change. It’s crucial for optimizing performance in computationally expensive operations.

Basic Syntax

import { useMemo } from 'react';

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

When to Use useMemo

1. Expensive Calculations

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

  return <div>Fibonacci({n}) = {fib}</div>;
}

2. Filtering and Sorting Large Lists

function ProductList({ products, filter, sortBy }) {
  const filteredAndSortedProducts = useMemo(() => {
    let result = [...products];
    
    if (filter) {
      result = result.filter(p => p.category === filter);
    }
    
    if (sortBy) {
      result.sort((a, b) => a[sortBy] - b[sortBy]);
    }
    
    return result;
  }, [products, filter, sortBy]);

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

3. Object Reference Stability

function UserSettings({ userId }) {
  const defaultSettings = useMemo(() => ({
    theme: 'dark',
    notifications: true,
    language: 'en'
  }), []);

  const [settings, setSettings] = useState(defaultSettings);

  // defaultSettings won't change on re-renders
  return <SettingsForm settings={settings} />;
}

Combining Hooks: A Complete Example

Here’s a practical example that combines multiple hooks:

function Dashboard({ userId }) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [filter, setFilter] = useState('all');
  const searchInputRef = useRef(null);
  const renderCount = useRef(0);

  // Track render count (debugging)
  renderCount.current += 1;

  // Fetch data when userId changes
  useEffect(() => {
    const fetchData = async () => {
      setLoading(true);
      try {
        const response = await fetch(`/api/users/${userId}/data`);
        const result = await response.json();
        setData(result);
      } catch (error) {
        console.error('Fetch failed:', error);
      } finally {
        setLoading(false);
      }
    };

    fetchData();
  }, [userId]);

  // Memoize filtered data
  const filteredData = useMemo(() => {
    if (!data) return [];
    
    return data.items.filter(item => {
      if (filter === 'all') return true;
      return item.category === filter;
    });
  }, [data, filter]);

  // Memoize filter handler
  const handleFilterChange = useCallback((newFilter) => {
    setFilter(newFilter);
  }, []);

  // Focus search input
  const focusSearch = () => {
    searchInputRef.current?.focus();
  };

  if (loading) return <div>Loading...</div>;

  return (
    <div>
      <input ref={searchInputRef} placeholder="Search..." />
      <button onClick={focusSearch}>Focus Search</button>
      
      <FilterSelector value={filter} onChange={handleFilterChange} />
      
      <DataList items={filteredData} />
      
      <small>Render count: {renderCount.current}</small>
    </div>
  );
}

Hook Comparison Table

HookPurposeRe-renders Component?Use When
useStateState managementYesYou need reactive state
useEffectSide effectsNo (but can trigger state updates)Data fetching, subscriptions, DOM manipulation
useCallbackMemoize functionsNoPassing stable callbacks to optimized children
useRefDOM access / mutable valuesNoDirect DOM access, storing mutable values
useMemoMemoize valuesNoExpensive computations, stable object references

Performance Tips

  1. Don’t optimize prematurely - Only use useMemo and useCallback when you have a performance issue or need reference stability.

  2. Keep dependency arrays accurate - Missing dependencies can cause bugs; extra dependencies can hurt performance.

  3. Use the ESLint plugin - Install eslint-plugin-react-hooks to catch common mistakes.

  4. Profile before optimizing - Use React DevTools Profiler to identify actual performance bottlenecks.

Conclusion

Mastering these five hooks—useState, useEffect, useCallback, useRef, and useMemo—gives you the foundation to build modern, efficient React applications. Remember:

  • useState for component state
  • useEffect for side effects
  • useCallback for stable function references
  • useRef for DOM access and mutable values
  • useMemo for expensive computations

Start with the basics, understand when each hook is appropriate, and optimize only when necessary. Happy coding!

Member discussion

0 comments

Start the conversation

Become a member of >hacksubset_ to start commenting.