All posts
reacthooks

React Hooks: A Practical Guide for Full-Stack Developers

A practical guide to React Hooks — setup, core concepts, common mistakes, and production tips for full-stack developers.

SR

Suhail Roushan

August 6, 2026

·
6 min read
·
0 views

React Hooks let you use state and lifecycle features in function components without rewriting class components — here's how to use them correctly in production.

React Hooks changed how we build React applications. Since React 16.8, hooks have become the standard way to manage state and side effects in function components. If you're a full-stack developer coming from class components or vanilla JavaScript, hooks simplify your mental model — but they also introduce new rules you can't ignore.

Why React Hooks Matters (and When to Skip It)

Hooks matter because they eliminate the ceremony of class components. No more this binding, no more componentDidMount vs componentDidUpdate confusion. You get a single mental model for state, effects, and context.

But here's my opinionated take: don't use hooks if you're maintaining a stable legacy codebase with zero new features. Refactoring working class components to hooks for "cleanliness" is a waste of time. The cost of regression bugs outweighs the aesthetic benefit. Use hooks for new code, not for vanity refactors.

Getting Started with React Hooks

You need React 16.8 or later. Here's the minimal setup with TypeScript:

npm install react@latest react-dom@latest
npm install --save-dev @types/react @types/react-dom

Your first hook — useState — in a runnable TypeScript component:

import { useState } from 'react';

interface CounterProps {
  initialCount?: number;
}

export function Counter({ initialCount = 0 }: CounterProps) {
  const [count, setCount] = useState<number>(initialCount);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount((prev) => prev + 1)}>Increment</button>
      <button onClick={() => setCount(initialCount)}>Reset</button>
    </div>
  );
}

That's it. No class, no this, no constructor. The functional updater (prev) => prev + 1 ensures you always work with the latest state, even under rapid clicks.

Core React Hooks Concepts Every Developer Should Know

1. useEffect — Taming Side Effects

useEffect runs after render. It replaces componentDidMount, componentDidUpdate, and componentWillUnmount in one API:

import { useState, useEffect } from 'react';

export function UserStatus({ userId }: { userId: string }) {
  const [status, setStatus] = useState<'online' | 'offline'>('offline');

  useEffect(() => {
    let isMounted = true; // Prevents state update after unmount

    const fetchStatus = async () => {
      const response = await fetch(`/api/users/${userId}/status`);
      const data = await response.json();
      if (isMounted) {
        setStatus(data.status);
      }
    };

    fetchStatus();

    return () => {
      isMounted = false; // Cleanup on unmount or userId change
    };
  }, [userId]); // Dependency array — only re-run when userId changes

  return <span>User is {status}</span>;
}

The dependency array is the key. Empty array [] means run once on mount. Missing dependencies cause stale closures — a classic bug we'll cover below.

2. useMemo and useCallback — Performance Without Premature Optimization

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

interface Product {
  id: number;
  name: string;
  price: number;
}

export function ProductList({ products }: { products: Product[] }) {
  const [filter, setFilter] = useState('');

  // Only recompute when products or filter change
  const filteredProducts = useMemo(() => {
    return products.filter((p) =>
      p.name.toLowerCase().includes(filter.toLowerCase())
    );
  }, [products, filter]);

  // Stable reference — use as prop for memoized child components
  const handleFilterChange = useCallback((value: string) => {
    setFilter(value);
  }, []);

  return (
    <div>
      <input
        value={filter}
        onChange={(e) => handleFilterChange(e.target.value)}
        placeholder="Filter products"
      />
      <ul>
        {filteredProducts.map((p) => (
          <li key={p.id}>{p.name} — ${p.price}</li>
        ))}
      </ul>
    </div>
  );
}

Use useMemo for expensive computations, not trivial ones. Use useCallback when passing functions to memoized child components. Otherwise, you're just adding overhead.

3. Custom Hooks — Composing Logic

Custom hooks are functions starting with use that call other hooks. This is where hooks shine for full-stack developers — you can share data-fetching logic across components:

import { useState, useEffect } from 'react';

interface FetchState<T> {
  data: T | null;
  loading: boolean;
  error: string | null;
}

export function useFetch<T>(url: string): FetchState<T> {
  const [state, setState] = useState<FetchState<T>>({
    data: null,
    loading: true,
    error: null,
  });

  useEffect(() => {
    const controller = new AbortController();

    const fetchData = async () => {
      try {
        const response = await fetch(url, { signal: controller.signal });
        if (!response.ok) throw new Error(`HTTP ${response.status}`);
        const data = await response.json();
        setState({ data, loading: false, error: null });
      } catch (err) {
        if (err instanceof Error && err.name !== 'AbortError') {
          setState({ data: null, loading: false, error: err.message });
        }
      }
    };

    fetchData();
    return () => controller.abort(); // Cancel request on unmount
  }, [url]);

  return state;
}

Now any component can use it: const { data, loading, error } = useFetch<User>('/api/users/42');

Common React Hooks Mistakes and How to Fix Them

Mistake 1: Missing Dependencies in useEffect

// ❌ BAD — stale closure, lint warning
useEffect(() => {
  setCount(count + 1);
}, []);

// ✅ GOOD — functional update
useEffect(() => {
  setCount((prev) => prev + 1);
}, []);

Functional updates avoid the stale closure entirely. If you need a value inside the effect, put it in the dependency array.

Mistake 2: Running Effects on Every Render

// ❌ BAD — fetch on every render, infinite loop risk
useEffect(() => {
  fetch('/api/data').then((r) => r.json()).then(setData);
});

// ✅ GOOD — run once with empty deps
useEffect(() => {
  fetch('/api/data').then((r) => r.json()).then(setData);
}, []);

Mistake 3: Mutating State Directly

// ❌ BAD — React won't re-render
const [user, setUser] = useState({ name: 'Suhail' });
user.name = 'Roushan'; // Mutating state directly

// ✅ GOOD — create a new object
setUser((prev) => ({ ...prev, name: 'Roushan' }));

React uses referential equality to detect changes. Mutating an object in place doesn't create a new reference, so the component won't re-render.

When Should You Use React Hooks?

Use React Hooks when you're writing function components that need state, side effects, or context. That's the default for any new React code today. You should also use hooks when you want to reuse stateful logic across components — custom hooks beat higher-order components and render props for readability. Skip hooks only if you're maintaining a legacy class-component codebase with no new features planned, or if your team has a strict policy against them.

React Hooks in Production

1. Use ESLint's react-hooks Plugin

npm install --save-dev eslint-plugin-react-hooks

Enable the rules-of-hooks and exhaustive-deps rules. They catch the dependency mistakes above at lint time, not runtime. This is non-negotiable in my projects.

2. Split Hooks by Responsibility

Don't cram everything into one useEffect. Separate concerns:

// ❌ Mixed concerns in one effect
useEffect(() => {
  fetchUser();
  trackAnalytics();
  subscribeToSocket();
}, []);

// ✅ Separate hooks
useEffect(() => {
  fetchUser();
}, [userId]);

useEffect(() => {
  trackAnalytics();
}, [page]);

useEffect(() => {
  const socket = subscribeToSocket();
  return () => socket.disconnect();
}, []);

3. Profile Before Optimizing with useMemo

Don't wrap everything in useMemo preemptively. Run the React Profiler first. If a computation takes under 1ms, useMemo adds more overhead than it saves. Measure, then optimize.

One concrete takeaway: adopt hooks for all new React code, install the ESLint plugin on day one, and always use functional updates with setState — these three habits will prevent 80% of the bugs I see in production React apps. For more full-stack patterns and real-world examples, check out the tutorials on suhailroushan.com.

Related posts

Written by Suhail Roushan — Full-stack developer. More posts on AI, Next.js, and building products at suhailroushan.com/blog.

Get in touch