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
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 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.
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>
);
}
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 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.
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 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.
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 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.
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 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.
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).
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.
| Mistake | What Happens | Solution |
|---|---|---|
| Hook in a condition | Hook order breaks | Move condition logic inside the hook |
| Stale closure | Effect uses old value | Add all dependencies to the array |
| Infinite loop | useEffect without dependencies changes state | Specify dependencies or use useReducer |
| Mutating ref in render | Side effect in function body | Move 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
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.
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).
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.
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.
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
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.
Read also