How to Use React Hooks: A Practical Guide to useState, useEffect, and Custom Hooks

If you have ever opened the React documentation, searched for how to use React hooks and walked away with a head full of theory but no clear idea how to apply it in a real project, this guide is for you. Instead of repeating the official docs, we are going to walk through the hooks you will actually use every day, the mistakes we see in code reviews at Coding4, and the moment when it becomes worth writing your own custom hook.

What React Hooks Actually Are (in plain English)

React hooks are functions that let you use React state and lifecycle features inside function components. Before hooks, you needed class components to manage state or run side effects. Today, function components plus hooks are the default way to build React applications, and class components are essentially legacy.

Two rules to remember before writing any hook:

  1. Only call hooks at the top level of your component or another hook. Never inside loops, conditions, or nested functions.
  2. Only call hooks from React functions: components or custom hooks. Not from regular JavaScript functions.

Break these rules and React loses track of which state belongs to which call. Most linting setups will catch this automatically with the eslint-plugin-react-hooks package, which you should always install.

react code laptop

useState: Managing Local State

The useState hook is the entry point for almost everyone learning hooks. It returns a value and a setter function.

import { useState } from 'react';

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

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

The Pitfall Nobody Warns You About

When your next state depends on the previous state, always use the functional form of the setter. Otherwise, you can read a stale value, especially inside async callbacks or rapid event handlers.

// Risky: stale closure
setCount(count + 1);

// Safe: always works with the latest value
setCount(prev => prev + 1);

Grouping State the Right Way

A common beginner reflex is to put everything into one object. In practice, you should split state by concern. If two pieces of state never change together, keep them in separate useState calls. It makes updates simpler and avoids accidentally overwriting fields.

useEffect: Synchronizing with the Outside World

The useEffect hook is where most bugs live in React applications. The mental model that actually works: an effect synchronizes your component with something external, like a network request, a subscription, the DOM, or a timer.

import { useState, useEffect } from 'react';

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

  useEffect(() => {
    let cancelled = false;

    fetch(`/api/users/${userId}`)
      .then(res => res.json())
      .then(data => {
        if (!cancelled) setUser(data);
      });

    return () => { cancelled = true; };
  }, [userId]);

  if (!user) return <p>Loading...</p>;
  return <h1>{user.name}</h1>;
}

Three Effect Pitfalls We See Constantly

  • Missing dependencies. Every value from the component scope that is used inside the effect must be listed. Suppressing the lint warning almost always hides a bug.
  • Forgetting cleanup. If you subscribe, set a timer, or start a fetch, return a cleanup function. Otherwise you leak memory or update unmounted components.
  • Using effects for derived data. If you can compute a value from props or state during render, do it during render. Do not put it in an effect.

When You Do Not Need useEffect

This is the single biggest improvement most codebases need. Here is a quick reference table:

Situation Use Effect? Better Option
Transforming data for rendering No Compute during render
Reacting to a user event No Handle in the event handler
Fetching data on mount Sometimes A data library like TanStack Query
Subscribing to an external store No useSyncExternalStore
Setting up a DOM listener Yes useEffect with cleanup
react code laptop

useRef: When You Need a Value That Survives Renders

Use useRef when you need a mutable value that should not trigger a re-render when it changes. Two classic uses: accessing a DOM node, and storing a value like a timer ID or the latest props between renders.

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

  useEffect(() => {
    inputRef.current?.focus();
  }, []);

  return <input ref={inputRef} />;
}

useMemo and useCallback: Performance, Not Magic

These two hooks are constantly misused. They do not make your app faster by default. They cache values and functions across renders, which only matters when:

  • The computation is genuinely expensive, or
  • The reference identity matters for a memoized child component or another hook dependency array.

If neither applies, skip them. Premature memoization adds complexity without benefit.

const sortedItems = useMemo(
  () => items.slice().sort((a, b) => a.price - b.price),
  [items]
);

const handleSelect = useCallback((id) => {
  onSelect(id);
}, [onSelect]);

useContext: Sharing Values Without Prop Drilling

Context is great for things that rarely change: the current user, theme, locale, feature flags. It is not a state management library. If your context value updates frequently and is consumed by many components, you will hit performance problems. For that, reach for a dedicated store like Zustand or Redux Toolkit.

react code laptop

Building Your First Custom Hook

A custom hook is just a function whose name starts with use and which calls other hooks. That is the entire definition. The point is to extract reusable logic so your components stay focused on rendering.

Here is a practical example we use in real projects: a useDebouncedValue hook for search inputs.

import { useState, useEffect } from 'react';

function useDebouncedValue(value, delay = 300) {
  const [debounced, setDebounced] = useState(value);

  useEffect(() => {
    const id = setTimeout(() => setDebounced(value), delay);
    return () => clearTimeout(id);
  }, [value, delay]);

  return debounced;
}

// Usage
function Search() {
  const [query, setQuery] = useState('');
  const debouncedQuery = useDebouncedValue(query, 400);

  useEffect(() => {
    if (debouncedQuery) {
      fetch(`/api/search?q=${debouncedQuery}`);
    }
  }, [debouncedQuery]);

  return <input value={query} onChange={e => setQuery(e.target.value)} />;
}

When Should You Actually Create a Custom Hook?

  1. You are copy pasting the same useState plus useEffect combination across components.
  2. The logic has a clear, nameable purpose (debouncing, pagination, form handling, online status).
  3. You want to test the logic in isolation from the UI.

Do not create a custom hook just to hide a single line of code. The abstraction has a cost. It should pay for itself.

A Sensible Mental Checklist Before You Ship

  • Did I list every dependency in my effect arrays?
  • Does every effect that needs cleanup return a cleanup function?
  • Am I using useEffect for something that could be done during render or in an event handler?
  • Am I memoizing for a real reason, or just out of habit?
  • Are my custom hooks named clearly with the use prefix?

FAQ

What are the two rules of React hooks?

Call hooks only at the top level of a component or another hook, and call them only from React function components or custom hooks. The official ESLint plugin enforces both rules.

When should I use React hooks instead of class components?

Always, for new code. Hooks are the standard since React 16.8 and the entire ecosystem, including the official documentation at react.dev, is built around them. Class components still work but should be considered legacy.

What are the most commonly used React hooks?

In production code, the order is roughly useState, useEffect, useRef, useContext, useMemo, and useCallback. Most components only need the first two.

How are React hooks different in React Native?

They are not. All the built-in hooks work identically in React Native. The only difference is that some custom hooks tied to the DOM, like ones using document or window, will not work without platform specific adjustments.

Should I use useEffect for data fetching?

It works for simple cases, but for anything serious you should reach for a dedicated library like TanStack Query or SWR. They handle caching, retries, deduplication, and stale data, which you would otherwise need to rebuild yourself.

Wrapping Up

Hooks are not complicated once you stop treating them as magic. They are functions with two rules, a few patterns, and a handful of pitfalls. Master useState and useEffect, learn when not to use an effect at all, and reach for a custom hook only when it genuinely simplifies your components. That is what separates React code that ages well from code that turns into a maintenance burden after six months.

If you want help auditing your React codebase or building a custom hook library for your team, the engineers at Coding4 do this every day. Get in touch and we will take a look.

Leave a Comment

Your email address will not be published. Required fields are marked *