BeginnerReact Concepts

What Are Hooks in React? – Your Brain’s Toolkit for Smarter Components

Hooks are special functions that let you use React features like state and effects inside functional components — replacing class lifecycles with clean, modern logic.

By Rudraksh Laddha

Introduction

In Virendana Ui, I was staring at a 400-line class component that handled user authentication, form validation, and real-time notifications. Every bug fix created two new issues. The constructor was a mess of binding methods, componentDidUpdate had nested conditionals, and testing felt impossible.

That's when I finally understood what React Hooks were really solving.


The Problem: Why Class Components Became Unmanageable

Before hooks, I spent most of my time wrestling with class component lifecycle methods. Logic was scattered across componentDidMount, componentDidUpdate, and componentWillUnmount. Related code lived in different methods, while unrelated code was forced together.

Hooks changed everything. They're functions that let you "hook into" React's state and lifecycle features from functional components. But more importantly, they let you organize code by what it does rather than when it runs.


Pattern 1: useState - State That Actually Makes Sense

I used to write this kind of constructor hell:

class Counter extends Component {
  constructor(props) {
    super(props);
    this.state = { count: 0 };
    this.handleClick = this.handleClick.bind(this);
  }
  
  handleClick() {
    this.setState({ count: this.state.count + 1 });
  }
}

Now I write this:

const [count, setCount] = useState(0);

The insight: useState returns exactly what you need - the current value and a setter. No binding, no constructor, no this.setState confusion. When you need multiple state values, use multiple useState calls instead of cramming everything into one object.

Production tip: I prefer multiple useState calls over one complex state object. It makes updates clearer and prevents the classic "forgot to spread the previous state" bug.


Pattern 2: useEffect - Side Effects Done Right

Here's how I handle the most common side effect - data fetching:

useEffect(() => {
  const fetchUser = async () => {
    try {
      const response = await api.getUser(userId);
      setUser(response.data);
    } catch (error) {
      setError(error.message);
    } finally {
      setLoading(false);
    }
  };

  fetchUser();
}, [userId]); // Only re-run when userId changes

Why this works: The dependency array [userId] tells React to only re-run this effect when userId changes. Without it, you'd fetch on every render. With an empty array [], it runs once after mount.

For cleanup (like removing event listeners):

useEffect(() => {
  const handleResize = () => setWindowWidth(window.innerWidth);
  
  window.addEventListener('resize', handleResize);
  
  return () => {
    window.removeEventListener('resize', handleResize);
  };
}, []);

Common mistake: Forgetting the dependency array. I've seen components make hundreds of API calls because the effect ran on every render. Always think: "What values does this effect depend on?"


Pattern 3: useRef - The Silent Keeper

useRef has two main uses. First, accessing DOM elements:

const inputRef = useRef();

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

return <input ref={inputRef} />;

Second, storing values that don't trigger re-renders:

const intervalRef = useRef();

useEffect(() => {
  intervalRef.current = setInterval(() => {
    // Do something every second
  }, 1000);
  
  return () => clearInterval(intervalRef.current);
}, []);

The key insight: useRef creates a mutable object that persists across renders. Unlike state, changing ref.current doesn't trigger a re-render. Perfect for timers, previous values, or DOM references.


Pattern 4: useContext - Global State Without Props Drilling

I used to pass user data through 5+ component levels. Now I use context:

const UserContext = createContext();

// In your app root
<UserContext.Provider value={{ user, setUser }}>
  <App />
</UserContext.Provider>

// In any child component
const { user, setUser } = useContext(UserContext);

When to use context: Theme settings, user authentication, language preferences - data that many components need. Don't use it for everything; local state is often better.

Performance note: Every context consumer re-renders when the context value changes. Split contexts if you have data that updates at different frequencies.


Pattern 5: useReducer - Complex State Logic

When useState gets messy, useReducer shines. I use it for forms, shopping carts, and any state with multiple related updates:

const [state, dispatch] = useReducer(reducer, initialState);

const formReducer = (state, action) => {
  switch (action.type) {
    case 'SET_FIELD':
      return { ...state, [action.field]: action.value };
    case 'SET_ERROR':
      return { ...state, errors: { ...state.errors, [action.field]: action.error } };
    case 'RESET':
      return initialState;
    default:
      return state;
  }
};

Why useReducer over useState: When state updates depend on previous state, when you have complex state logic, or when you need to dispatch actions from deeply nested components.


Pattern 6: useEffectEvent – Stable State Inside Effects (No Re-Runs)

Whenever your useEffect needs the latest state, but you don’t want the effect to re-run again and again — this is where useEffectEvent becomes a lifesaver. It gives you a stable event function that always reads the latest state without going inside the dependency chaos.

import { useState, useEffect, useEffectEvent } from "react";

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

  // Stable event (always sees fresh state)
  const logCount = useEffectEvent(() => {
    console.log("Count:", count);
  });

  useEffect(() => {
    const interval = setInterval(() => {
      logCount(); // No stale closure + effect doesn't re-run
    }, 1000);

    return () => clearInterval(interval);
  }, []);

  return <button onClick={() => setCount(count + 1)}>Increase {count}</button>;
}

Why useEffectEvent instead of putting state in effect dependencies?

Because when you include state in dependency array:

  • Timers restart
  • Socket listeners recreate
  • API polling resets

useEffectEvent keeps the effect stable, while still reading the latest state.

In short: When your effect should run once, but needs fresh state always → use useEffectEvent.


The Rules (And Why They Matter)

  • Only call hooks at the top level - React relies on call order to track hook state between renders
  • Only call hooks from React functions - They won't work in regular JavaScript functions
  • Custom hooks must start with "use" - This lets React's linter catch hook rule violations

These aren't arbitrary rules. React uses the order of hook calls to associate state with the right hooks. Call them conditionally, and you'll get the wrong state values.


Real-World Impact: Before and After

That 400-line authentication component I mentioned? After refactoring with hooks:

  • Reduced to 3 smaller, focused components
  • Logic grouped by purpose, not lifecycle
  • Unit tests went from impossible to straightforward
  • New team members could understand it in minutes, not hours

The secret isn't just using hooks - it's using them to organize your code around what it does, not when it runs.


Quick Reference: Hook Patterns

Hook Use When Common Pattern
useState Simple state updates Form inputs, toggles, counters
useEffect Side effects after render API calls, subscriptions, timers
useRef Persistent values without re-render DOM access, storing previous values
useContext Shared state across components Theme, auth, language settings
useReducer Complex state logic Forms, shopping carts, wizards
useEffectEvent When effect needs latest state without re-running Timers, socket listeners, analytics callbacks

Common Questions From My Experience

Q: Should I convert all my class components to hooks?

Not necessarily. If a class component works and isn't causing problems, leave it. Focus on new components and problematic legacy code. I've seen teams waste weeks on unnecessary conversions.

Q: How do I know when to use useReducer vs useState?

If you're writing useState with complex objects or multiple related state updates, consider useReducer. The reducer pattern makes state changes more predictable and easier to test.

Q: Can I use hooks for everything Redux does?

For many apps, yes. useContext + useReducer can replace Redux for medium-complexity state. But Redux still wins for complex apps with middleware, time-travel debugging, or strict state management requirements.

Q: Why do my effects run on every render?

Check your dependency array. Missing dependencies or creating new objects/functions inside the component are the usual culprits. Use useCallback and useMemo when necessary.


Key Takeaways

Hooks aren't just a different way to write React components - they're a better way to organize your code. Instead of scattering related logic across lifecycle methods, you group it by purpose. Instead of complex inheritance patterns, you compose behavior with custom hooks.

The mental shift from "when does this run?" to "what does this do?" transformed how I write React. Your components become more focused, your code becomes more reusable, and your debugging sessions become much shorter.

Start with useState and useEffect. Master those patterns, then gradually introduce useRef, useContext, and useReducer as your needs grow. The goal isn't to use every hook - it's to use the right hook for each job.

❤️ At Learn Virendana, we love creating high-quality React tutorials that simplify complex concepts and deliver a practical, real-world React learning experience for developers