React Hooks in Depth
Published on January 28, 2026
Written by: Code Arc Studio Editorial Team

Introduced in React 16.8, Hooks are functions that let you "hook into" React state and lifecycle features from function components. They revolutionized how developers write React applications by providing a more direct, powerful, and composable API to the React features we already know: props, state, context, refs, and lifecycle. Hooks allow you to extract stateful logic from a component so it can be tested independently and reused, all without the boilerplate of class components or complex patterns like higher-order components.
Before Hooks, stateful logic was primarily handled in class components, which could lead to large, complex components that were hard to test and understand (the "wrapper hell" problem). Hooks solve these problems by enabling better composition and reuse of logic, leading to cleaner, more readable, and more maintainable codebases.
The State Hook: 'useState'
The most fundamental hook is useState. It allows you to add a state variable to your component. React will preserve this state between re-renders. It returns an array with two elements: the current state value, and a function to update that value.
const [count, setCount] = useState(0);
// When updating state based on the previous state, use the functional update form:
const increment = () => {
setCount(prevCount => prevCount + 1);
};
Using the functional update form (setCount(prevCount => ...)) is crucial when your new state depends on the previous state, as it prevents bugs related to stale state in asynchronous operations.
The Effect Hook: 'useEffect'
The useEffect hook lets you perform "side effects" in your components. The most common examples are fetching data from an API, setting up a subscription (like a timer), or manually changing the DOM. The useEffect hook is a combination of componentDidMount, componentDidUpdate, and componentWillUnmount from the class component world, all unified into a single API.
useEffect(() => {
// This effect runs after every render where 'userId' has changed.
const controller = new AbortController();
const signal = controller.signal;
fetch(`https://api.example.com/users/${userId}`, { signal })
.then(res => res.json())
.then(setData);
// Return a cleanup function. This runs before the component unmounts
// or before the effect runs again for the next 'userId'.
return () => {
controller.abort(); // Abort the fetch to prevent memory leaks.
};
}, [userId]); // The dependency array.
The dependency array is the most critical part of useEffect. It tells React when to re-run the effect. An empty array [] means the effect runs only once after the initial render. If you omit the array, the effect runs after every single render. Properly managing dependencies and providing cleanup functions is key to writing bug-free effects.
Common React Hooks
| Hook | Purpose |
|---|---|
useState |
Manages state within a component. |
useEffect |
Handles side effects like data fetching or subscriptions. |
useContext |
Accesses data from a React Context without prop drilling. |
useReducer |
An alternative to useState for managing complex state logic. |
useCallback / useMemo |
Memoize functions and values to optimize performance. |
useRef |
Accesses DOM nodes directly or holds a mutable value that doesn't trigger a re-render. |
Creating Your Own Custom Hooks
The true power and elegance of Hooks shines when you create your own. A custom hook is simply a JavaScript function whose name starts with "use" and that calls other hooks internally. They are a convention for reusing stateful logic between components.
Let's create a hook to encapsulate the logic for fetching data, including loading and error states.
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
setLoading(true);
setError(null);
const controller = new AbortController();
fetch(url, { signal: controller.signal })
.then(res => {
if (!res.ok) throw new Error('Network response was not ok');
return res.json();
})
.then(setData)
.catch(setError)
.finally(() => setLoading(false));
return () => controller.abort();
}, [url]); // Re-run the effect if the URL changes
return { data, loading, error };
}
// Now, any component can use this hook to fetch data:
function UserProfile({ userId }) {
const { data: user, loading, error } = useFetch(`https://api.example.com/users/${userId}`);
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: ${error.message}</p>;
return <h1>Welcome, {user.name}</h1>;
}
This custom useFetch hook encapsulates all the logic for fetching, loading, and error handling into a clean, reusable, and easy-to-use function. This is the essence of the Hooks pattern: building complex, stateful applications by composing small, focused, and independent pieces of logic.
10 Common React Hooks Errors and Their Fixes
Hooks are powerful, but they come with rules. Breaking them can lead to confusing bugs. Here are 10 common mistakes and their solutions.
| # | Common Error | Why It Happens | Solution |
|---|---|---|---|
| 1 | Calling Hooks Conditionally | React relies on a consistent call order for Hooks on every render. Calling them inside an if, loop, or nested function violates this rule. |
Always call Hooks at the top level of your component. You can put the conditional logic *inside* the Hook if needed. |
| 2 | Missing useEffect Dependencies |
The effect uses a value from the component scope (props or state) that isn't listed in the dependency array, leading to stale closures where the effect uses an old value. | Add every reactive value used inside the effect to its dependency array. The official eslint-plugin-react-hooks is essential for catching this. |
| 3 | Creating an Infinite Loop with useEffect |
The effect updates a state variable that is also in its own dependency array, causing the effect to run again, which updates the state again, and so on. | If you need to update state based on a previous value, use the functional update form (setState(prev => ...)). If the dependency is an object or array, memoize it with useMemo or useCallback so it doesn't trigger the effect on every render. |
| 4 | Forgetting the useEffect Cleanup Function |
When a component unmounts, any ongoing processes started in useEffect (like subscriptions, timers, or API calls) can lead to memory leaks or errors if not cleaned up. |
Return a function from your useEffect. This cleanup function will run when the component unmounts or before the effect re-runs. |
| 5 | Using State for Data That Doesn't Need to Re-render | Storing values in state that don't affect the rendered output (like a timer ID or a mutable object) causes unnecessary re-renders. | Use the useRef hook to store values that you want to persist across renders without triggering a re-render. E.g., const timerIdRef = useRef(null);. |
| 6 | Passing a New Function or Object as a Prop on Every Render | Creating a function or object directly in the render body (e.g., <MyComponent style={{...}} />) causes child components that rely on that prop for memoization (like React.memo) to re-render unnecessarily. |
Wrap the function in useCallback and the object/array in useMemo to ensure they maintain the same reference between renders unless their own dependencies change. |
| 7 | Using Stale State in a Callback | An event handler is created in one render, capturing the state from that render. If the state updates later, the old handler still has the old state value. | Use the functional update form with your state setter (setCount(prevCount => prevCount + 1)). React guarantees the prevCount will be the latest state. |
| 8 | Treating useState as Asynchronous |
Calling setState does not immediately change the state variable in the currently running function. The new state is only available on the *next* render. |
If you need to perform an action immediately after a state change, use a useEffect hook that has that state variable in its dependency array. |
| 9 | Overusing useContext |
While Context is great for avoiding prop drilling, wrapping your entire app in a huge context can lead to performance issues, as any component consuming the context will re-render whenever the context value changes. | Keep contexts small and focused on a specific piece of state. Consider splitting large contexts into smaller, more granular ones. |
| 10 | Not Naming Custom Hooks with "use" | Custom Hooks must start with "use" (e.g., useFetch). This is not just a convention; it's a rule that allows ESLint plugins to check for violations of the rules of Hooks. |
Always start your custom Hook names with the "use" prefix. |