React Hooks — what they are, key hooks and usage rules

Author: IT Sectr Published: 2026-07-04 Reading time: 10 min

React Hooks are functions that let you use state, effects, context, and other React features in functional components, without writing classes. Introduced in React 16.8, hooks radically changed the development approach, replacing complex patterns like HOC and render props. According to React, 2024, there are 15 built-in hooks covering all component management scenarios.

Key Takeaways

  • Hooks — functions for connecting React features to functional components without extends Component.
  • useState — the basic hook for state management, returns a value and a setter.
  • useEffect — for side effects: API requests, subscriptions, timers, DOM manipulations.
  • useContext — for accessing React context without Consumer wrappers and static contextType.
  • Rules of Hooks — call only at the top level and only inside functional components or custom hooks.

What Are React Hooks?

React Hooks are a set of functions built into React that let you “hook into” the framework’s internal mechanisms without creating classes. Before React 16.8, functional components could only accept props and return JSX — any complex logic required a class component.

The problem with class components is splitting related logic. Code related to one entity (e.g., a WebSocket subscription) was forcibly split between componentDidMount, componentDidUpdate, and componentWillUnmount. Hooks solve this: all logic is grouped in one useEffect.

The second problem is logic reuse. To reuse state logic between class components, you had to apply HOC (Higher-Order Components) or render props — both patterns created extra nesting in the component tree. Custom hooks solve this without nesting.

useState — State Management

useState is the most basic hook. It takes an initial value and returns a tuple: the current value and a function to update it. Unlike this.setState in classes, useState does not merge objects — the new state completely replaces the previous one for a specific useState call.

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

function Form() {
  const [text, setText] = useState('');
  const [submitting, setSubmitting] = useState(false);

  async function handleSubmit() {
    setSubmitting(true);
    await fetch('/api/submit', {
      method: 'POST',
      body: JSON.stringify({ text })
    });
    setSubmitting(false);
  }

  return (
    <div>
      <input
        value={text}
        onChange={e => setText(e.target.value)}
      />
      <button onClick={handleSubmit}
        disabled={submitting}>
        {submitting ? 'Saving...' : 'Submit'}
      </button>
    </div>
  );
}

Functional setState Form

The useState setter can accept a function that receives the previous value and returns a new one. The functional form is required when the new state is computed from the previous one and batched setState calls are possible — each call will receive the current previous value.

useEffect — Side Effects and Lifecycle

useEffect is a hook for performing side effects in functional components. It takes two arguments: an effect function and a dependency array. React runs the effect after each render, but when dependencies are specified — only when any of them changes.

An empty dependency array [] means the effect runs once after the first render (analogous to componentDidMount). Returning a function from the effect is a cleanup that runs on unmount (analogous to componentWillUnmount) and before re-running the effect.

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

function ChatRoom({ roomId }) {
  const [messages, setMessages] = useState([]);

  useEffect(() => {
    const socket = new WebSocket(`wss://chat/${roomId}`);

    socket.onmessage = (event) => {
      setMessages(prev => [...prev, JSON.parse(event.data)]);
    };

    // Cleanup - close socket
    return () => socket.close();
  }, [roomId]);

  return <ul>
    {messages.map((msg, i) =>
      <li key={i}>{msg.text}</li>
    )}
  </ul>;
}

useEffect dependencies are the only connection between the effect and the outside world. If the effect uses a variable from props or state, it must be in the dependency array. Ignoring this rule is a source of bugs with stale closures.

useContext — Accessing Context

useContext is a hook for reading context values. It takes a context object created by React.createContext and returns the current value. If the context changes, the component automatically re-renders with the new value.

Before useContext, accessing context in functional components required wrapping in Context.Consumer, which created nesting. useContext completely eliminates Consumer — just call the hook anywhere in the functional component.

js
import React, { createContext, useContext } from 'react';

const ThemeContext = createContext('light');

function ThemedButton() {
  const theme = useContext(ThemeContext);

  return <button
    className={`btn btn-${theme}`}
  >
    {theme === 'dark' ? 'moon' : '☀️'}
  </button>;
}

useContext is not a replacement for Redux or global state. React recommends using context for infrequently changing values — theme, locale, auth status. For frequently updated data, each re-render will recalculate all context consumers, which may reduce performance.

useReducer — Complex State Logic

useReducer is an alternative to useState for states with complex update logic. It takes a reducer function and initial state, returns the current state and a dispatch function. The reducer receives the current state and action, returns a new state — a pure function without side effects.

useReducer is especially useful when multiple actions (set, reset, increment, addItem) manage one state. Instead of multiple useState calls with different setters — one reducer with clear action types. This makes logic predictable and easily testable.

js
import React, { useReducer } from 'react';

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return { ...state, count: state.count + 1 };
    case 'setStep':
      return { ...state, step: action.step };
    case 'reset':
      return { count: 0, step: 1 };
    default:
      throw new Error('Unknown action: ' + action.type);
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, {
    count: 0,
    step: 1
  });

  return (
    <>
      <p>Count: {state.count}</p>
      <button
        onClick={() => dispatch({ type: 'increment' })}>+</button>
      <button
        onClick={() => dispatch({ type: 'reset' })}>Reset</button>
    </>
  );
}

useRef, useMemo, useCallback — Optimization

useRef creates a mutable object that persists between renders. Changing current does not trigger a re-render. The main use cases are accessing DOM elements and storing mutable values (timers, previous values, library instances).

useMemo memoizes a function’s result. It recalculates the value only when a dependency changes. Use it for expensive computations — filtering large arrays, sorting, data formatting. useCallback is a special case of useMemo for memoizing functions.

js
import React, { useRef, useMemo, useCallback } from 'react';

function SearchPage({ items, query, onItemSelect }) {
  const inputRef = useRef(null);

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

  // Memoize filtered result
  const filtered = useMemo(() => {
    return items.filter(item =>
      item.name.toLowerCase()
        .includes(query.toLowerCase())
    );
  }, [items, query]);

  // Memoize callback
  const handleSelect = useCallback((id) => {
    onItemSelect(id);
  }, [onItemSelect]);

  return (
    <div>
      <input ref={inputRef}
        value={query}
        onChange={e => setQuery(e.target.value)}
      />
      <ul>
        {filtered.map(item =>
          <li key={item.id}>
            <button
              onClick={() => handleSelect(item.id)}>
              {item.name}
            </button>
          </li>
        )}
      </ul>
    </div>
  );
}

An important rule: useRef, useMemo, and useCallback are optimization tools, not mandatory constructs. Don’t wrap everything in useMemo — measure performance first. Premature optimization complicates code. React renders components quickly; useMemo is only needed when computations are truly expensive (O(n²) and above).

Rules of Hooks and Common Mistakes

React introduced two strict rules for hooks. First: call hooks only at the top level — not inside conditions, loops, or nested functions. Second: call hooks only from React functional components or custom hooks. The ESLint plugin eslint-plugin-react-hooks automatically checks both rules.

The most common mistake is violating the order of hook calls. React relies on the order of hook calls between renders. If a hook is inside a condition and doesn’t fire in one render, all subsequent hooks shift, causing bugs.

MistakeWhat HappensSolution
Hook in a conditionHook order breaksMove condition logic inside the hook
Stale closureEffect uses old valueAdd all dependencies to the array
Infinite loopuseEffect without dependencies changes stateSpecify dependencies or use useReducer
Mutating ref in renderSide effect in function bodyMove mutations to useEffect

The second common mistake is stale closure in useEffect. If the effect uses a variable not specified in dependencies, the effect “captures” its value at the time of closure creation. The solution is to always include all used variables in the dependency array. If there are too many dependencies, consider splitting the effect into several.

Frequently Asked Questions

Why can’t hooks be called inside conditions?

React relies on the order of hook calls between renders. If a hook is called inside a condition and doesn’t fire in one render, all subsequent hooks shift — their state gets mixed up. The ESLint rule hooks/exhaustive-deps warns about this.

What is the difference between useEffect and useLayoutEffect?

useLayoutEffect is synchronous and runs before the browser paints changes. useEffect is asynchronous — after painting. Use useLayoutEffect only when you need to measure or change DOM before the user sees changes (popover position, scroll restoration).

How to cancel a fetch request on unmount?

Use AbortController inside useEffect. Create a controller, pass the signal to fetch, and call controller.abort() in the cleanup function. For older browsers, use a cancelled flag: return () => { cancelled = true; } with a check before setState.

What is a custom hook and when to create one?

A custom hook is a JavaScript function with the use prefix that uses built-in hooks. Create one when the same logic with hooks repeats in two or more components. Examples: useDebounce, useLocalStorage, useMediaQuery. Custom hooks replace HOC and render props.

Why does useEffect run twice in Strict Mode?

In Strict Mode, React intentionally mounts and unmounts the component twice in development to find bugs with uncleaned effects. If the cleanup is written correctly, the double call causes no problems. This does not happen in production mode.

Summary

  • React Hooks — functions for using state, effects, and context in functional components, replacing classes.
  • useState — basic hook for local state with a functional setter for safe updates.
  • useEffect — universal hook for side effects with a dependency array and cleanup function.
  • useContext — direct access to React context without Consumer wrappers and extra nesting.
  • useReducer — for complex state logic with action types, an alternative to Redux within a single component.
  • useRef, useMemo, useCallback — optimization tools: DOM references, value and callback memoization.
  • Top-level rule — strict restriction on calling hooks outside conditions, loops, and nested functions.

We will develop a mobile application turnkey

IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.

Discuss the project

Read also