DEV Community

Cover image for Advancing with React: Hooks Deep Dive! (React Day 5)
Vasu Ghanta
Vasu Ghanta

Posted on

Advancing with React: Hooks Deep Dive! (React Day 5)

Hey, React enthusiasts! Welcome back from Day 4, where we powered up with lists, keys, and component patterns. Today, we're diving deep into the heart of modern React: Hooks! We'll explore essential hooks like useEffect for side effects, useRef for direct DOM access, useCallback and useMemo for optimization, and how to create custom hooks for reusable logic. Along the way, we'll unpack lifecycle behavior in functional components using hooks, dissect dependency arrays, tackle performance optimizations, highlight common pitfalls, and compare everything to class components. Expect code examples you can test in your Vite setup, visuals for lifecycle flows, and real-world applications to solidify your understanding. By the end, you'll wield hooks like a pro, making your apps more efficient and maintainable. Let's hook in!

Hooks Overview: Revolutionizing Functional Components

Hooks, introduced in React 16.8, are functions that let you "hook into" React features like state and lifecycle from functional components. No more classes for most scenarios—hooks keep code concise and composable.

Why They Matter: Before hooks, functional components were stateless. Hooks bridge that gap, enabling state, effects, and more without the boilerplate of class methods like componentDidMount.

Lifecycle Behavior with Hooks: Unlike class components with explicit methods (e.g., componentDidMount, componentDidUpdate, componentWillUnmount), hooks simulate lifecycles declaratively. The main player is useEffect, which runs after renders based on dependencies. Components "mount" on first render, "update" on re-renders, and "unmount" when removed.

Here's a visual for the functional component lifecycle with hooks:

React Function Component Lifecycle

Comparison with Class Components: Classes use rigid methods; hooks are flexible—combine multiple useEffect for separated concerns. Classes suffer from this binding; hooks don't. However, classes might feel more intuitive for OOP fans, but hooks promote better code reuse.

Real-World Scenario: In a dashboard app, hooks manage data fetching on mount and cleanup on unmount, replacing scattered class lifecycle methods with focused effects.

useEffect: Mastering Side Effects and Lifecycle

useEffect runs code after render, handling side effects like data fetching, subscriptions, or DOM mutations. It's your go-to for lifecycle simulation in functions.

How It Works: Pass a callback; it runs post-render. Return a cleanup function for unmount. The dependency array controls when it re-runs: empty [] for mount only, [deps] for on-change, none for every render.

Live Example: Fetch data on mount:

import React, { useState, useEffect } from 'react';

function DataFetcher() {
  const [data, setData] = useState(null);

  useEffect(() => {
    fetch('https://api.example.com/data')
      .then(res => res.json())
      .then(setData);
    return () => console.log('Cleanup: Abort fetch if needed');
  }, []); // Empty deps: Runs once on mount

  return <div>{data ? JSON.stringify(data) : 'Loading...'}</div>;
}
Enter fullscreen mode Exit fullscreen mode

Paste this into a component—watch it fetch once!

Dependency Arrays: They prevent infinite loops. Include all used vars; skip stables like setState. Wrong deps? Stale data or excess runs.

Performance Optimization: Use for async ops, but debounce/throttle if deps change often. Avoid heavy computations inside.

Common Mistakes: Forgetting cleanup (leaks subscriptions), mutating state in effect without deps (loops), or ignoring ESLint warnings about missing deps.

Comparison with Class Components: Replaces componentDidMount, componentDidUpdate, and componentWillUnmount in one hook. More granular—no single didUpdate for all props.

Here's a cheatsheet comparing lifecycle methods to hooks:

React lifecycle methods with hooks cheatsheet

Real-World Scenario: In a chat app, useEffect subscribes to WebSockets on mount, updates messages on data, and unsubscribes on unmount—clean and efficient.

useRef: Persistent References Without Re-renders

useRef creates a mutable ref object that persists across renders, ideal for DOM access or storing values without triggering updates.

How It Works: Returns { current: initialValue }. Change .current anytime; it won't re-render.

Live Example: Focus input on button click:

import React, { useRef } from 'react';

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

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

  return (
    <>
      <input ref={inputRef} type="text" />
      <button onClick={handleClick}>Focus</button>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

No state needed—ref handles it!

Lifecycle Behavior: Refs survive re-renders, mimicking instance vars in classes. Use for intervals/timers that outlive renders.

Common Mistakes: Overusing for state (use useState instead), or forgetting refs aren't reactive—no auto-updates on change.

Comparison with Class Components: Like this.myRef = React.createRef() in constructor, but hooks version is simpler without this.

Real-World Scenario: In a video player, useRef stores the video element for direct play/pause control, avoiding prop drilling or state bloat.

useCallback and useMemo: Turbocharging Performance

These memoization hooks cache functions (useCallback) or values (useMemo) to prevent unnecessary re-creations, optimizing child re-renders.

How They Work: Both take a creator and deps. useCallback returns a memoized callback; useMemo returns a memoized value.

Live Example: Memoized handler to avoid child re-renders:

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

function Parent() {
  const [count, setCount] = useState(0);
  const handleClick = useCallback(() => setCount(c => c + 1), []); // Memoized

  return (
    <div>
      <Child onClick={handleClick} />
      <p>Count: {count}</p>
    </div>
  );
}

const Child = React.memo(({ onClick }) => <button onClick={onClick}>Increment</button>);
Enter fullscreen mode Exit fullscreen mode

Without useCallback, Child re-renders on every Parent render!

For useMemo: Compute expensive value:

const memoizedValue = useMemo(() => expensiveComputation(a, b), [a, b]);
Enter fullscreen mode Exit fullscreen mode

Dependency Arrays: Crucial—wrong deps mean stale callbacks/values or no optimization.

Performance Optimization: Use for props to memoized children (via React.memo). But don't overuse—premature optimization adds complexity.

Common Mistakes: Memoizing everything (overhead), or forgetting deps (stale closures). Test with React DevTools profiler.

Comparison with Class Components: Classes use method binding or PureComponent, but hooks are more explicit and flexible for functions/values.

Real-World Scenario: In a data table, useMemo caches sorted/filtered data; useCallback wraps sort handlers passed to columns—prevents re-renders on typing.

Custom Hooks: Encapsulating Reusable Logic

Custom hooks are functions starting with "use" that call other hooks, sharing stateful logic without components.

How to Build: Extract logic into a function, e.g., useFetch for data loading.

Live Example: Custom fetch hook:

import { useState, useEffect } from 'react';

function useFetch(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);

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

  return { data, loading };
}

function App() {
  const { data, loading } = useFetch('https://api.example.com');
  return loading ? <p>Loading...</p> : <div>{JSON.stringify(data)}</div>;
}
Enter fullscreen mode Exit fullscreen mode

Reusable across components!

Best Practices: Keep pure (no renders), name with "use", handle cleanup. Share via npm for teams.

Common Mistakes: Calling hooks conditionally (rules of hooks), or coupling too tightly to UI.

Comparison with Class Components: Replaces mixins/HOCs, which were messier. Hooks compose cleaner.

Real-World Scenario: In a form-heavy app, a useForm hook manages validation, submission, and errors—reusable for login, signup, etc.

Here's a comparison of class vs. functional component lifecycles:

Component lifecycle in React — Class component vs Functional ...

Wrapping Up: Hooks Unlock React's Full Potential

You've conquered hooks—from useEffect's lifecycle magic to optimization with useCallback/useMemo, refs for persistence, and custom hooks for reuse. Compared to classes, hooks streamline code while offering powerful patterns. Experiment: Refactor a class component to hooks! Watch for dependency pitfalls and optimize wisely. Next, Day 6: Routing and Navigation. What's your top hook takeaway? Keep hooking! 🚀

Top comments (0)