Why Hooks Change How You Structure Components
React Hooks, introduced in React 16.8, let function components use state and other React features without writing a class component. They are functions that "hook into" React state and lifecycle features — but they only work inside function components, not class components.
React ships with built-in Hooks like useState, and you can also build your own Hooks to share stateful logic across components. A simple counter shows the core idea: the component declares state with useState and updates it via the returned setCount function.
See the Pen [React Hook example with Counter](https://codepen.io/smashingmag/pen/QWbXMyM) by Adeneye Abiodun David.
Here, the counter starts at 0. Each button click calls setCount to increment the value by 1.
const [count, setCount] = useState(0)
The onClick() handler invokes setCount to trigger the update.
<button onClick={() => setCount(count + 1)}>
Click me
</button>
Before Hooks, the same behavior would have required significantly more code inside a class component.
The Two Non-Negotiable Rules
Hooks are JavaScript functions, but they come with two hard rules you must follow:
- Call Hooks only at the top level of your component.
- Call Hooks only from React components — or from custom Hooks.
These rules are enforced by the eslint-plugin-react-hooks plugin. It ships with Create React App by default, so you get it automatically if you bootstrap your project that way. For other setups, you can add it manually.
// Your ESLint configuration
{
"plugins": [
// ...
"react-hooks"
],
"rules": {
// ...
"react-hooks/rules-of-hooks": "error", // Checks rules of Hooks
"react-hooks/exhaustive-deps": "warn" // Checks effect dependencies
}
}
Keep Hook Call Order Stable
Never call Hooks inside loops, conditions, or nested functions. Hooks must run at the top level of your React function so they execute in the same order on every render. React relies on that consistent ordering to preserve state across multiple useState and useEffect calls.
Consider a Form component with two state values — accountName and accountDetail — that are persisted to browser storage and the document title via useEffect.
function Form() {
// 1. Use the accountName state variable
const [accountName, setAccountName] = useState('David');
// 2. Use an effect for persisting the form
useEffect(function persistForm() {
localStorage.setItem('formData', accountName);
});
// 3. Use the accountDetail state variable
const [accountDetail, setAccountDetail] = useState('Active');
// 4. Use an effect for updating the title
useEffect(function updateStatus() {
document.title = accountName + ' ' + accountDetail;
});
// ...
}
As long as the Hook order never changes, React can correctly track which state belongs to which Hook call.
// ------------
useState('David') // 1. Initialize the accountName state variable with 'David'
useEffect(persistForm) // 2. Add an effect for persisting the form
useState('Active') // 3. Initialize the accountdetail state variable with 'Active'
useEffect(updateStatus) // 4. Add an effect for updating the status
// -------------
// Second render
// -------------
useState('David') // 1. Read the accountName state variable (argument is ignored)
useEffect(persistForm) // 2. Replace the effect for persisting the form
useState('Active') // 3. Read the accountDetail state variable (argument is ignored)
useEffect(updateStatus) // 4. Replace the effect for updating the status
// ...
Problems arise when a Hook is skipped conditionally. If a persistForm effect only runs when accountName !== '', the first render will execute it — but if the user later clears the form, the condition becomes false and that Hook disappears from the call sequence.
// 🔴 We're breaking the first rule by using a Hook in a condition
if (accountName !== '') {
useEffect(function persistForm() {
localStorage.setItem('formData', accountName);
});
}
Now the Hook order is different on subsequent renders:
useState('David') // 1. Read the accountName state variable (argument is ignored)
// useEffect(persistForm) // 🔴 This Hook was skipped!
useState('Active') // 🔴 2 (but was 3). Fail to read the accountDetails state variable
useEffect(updateStatus) // 🔴 3 (but was 4). Fail to replace the effect
React no longer knows what to return for the second useState call. It expects the second Hook to be the persistForm effect, matching the previous render — but it's not there. Every Hook call after the skipped one shifts by a position, producing subtle bugs.
The fix is simple: keep all Hooks at the top level of the component, and move any condition inside the Hook itself rather than wrapping the Hook call.
Hooks Belong in React Contexts
Regular JavaScript functions cannot call Hooks. A function only becomes a React component when React is imported and the function is used as a component — it's the React context that makes Hooks like useState available.
import { useState } = "react";
function toCelsius(fahrenheit) {
const [name, setName] = useState("David");
return (5/9) * (fahrenheit-32);
}
document.getElementById("demo").innerHTML = toCelsius;
Without that component context, importing a Hook does nothing useful; the Hook is bound to React's rendering lifecycle.
import React, { useState} from "react";
import ReactDOM from "react-dom";
function Account(props) {
const [name, setName] = useState("David");
return <p>Hello, {name}! The price is <b>{props.total}</b> and the total amount is <b>{props.amount}</b></p>
}
ReactDom.render(
<Account total={20} amount={5000} />,
document.getElementById('root')
);
Custom Hooks Extend the Same Rules
A custom Hook is just a JavaScript function whose name starts with use and which may call other Hooks. For example, a useUserName custom Hook could fetch API data, iterate over results, and call setIsPresent() to check whether a given username exists.
export default function useUserName(userName) {
const [isPresent, setIsPresent] = useState(false);
useEffect(() => {
const data = MockedApi.fetchData();
data.then((res) => {
res.forEach((e) => {
if (e.name === userName) {
setIsPresent(true);
}
});
});
});
return isPresent;
}
Once that logic lives in a custom Hook, you can reuse it anywhere without re-implementing useState or useEffect calls. This keeps stateful logic visible and explicit in your component source.
From Lifecycle Thinking to Hook Composition
The simplest React component is a plain JavaScript function that accepts props and returns a React element.
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
Class components follow a different model. They rely on encapsulation — bundling data with the methods that operate on it — and organize behavior around lifecycle methods like constructors, componentDidMount(), and render.
Hooks shift that mental model. Instead of structuring a component by lifecycle stage, you compose it from functional pieces that each end with some rendered output. A component built this way can manage its own state and side effects without the ceremony of a class.
function {
useHook{...};
useHook{...};
useHook{...};
return (
...
);
}
A class component requires extending React.Component and implementing a render method that returns an element. That structure gives you more boilerplate alongside its benefits.
class {
constructor(props) {...}
componentDidMount() {...}
componentWillUnmount() {...}
render() {...}
}
Function components with Hooks offer concrete advantages:
- Separating container and presentational components becomes straightforward, since you must think more explicitly about state when you don't have direct access to
setState(). - They are plain JavaScript functions with no state or lifecycle hooks, making them easier to read and test.
- They require less code overall.
- React's team has noted potential performance gains for function components in future releases.
Keep Hooks Lean
The simplest Hook is often the best one. Before reaching for a custom Hook, consider whether an inline useState() or useEffect() will do the job. Avoid wrapping trivial logic in a custom Hook just for the sake of abstraction.
When you do need a custom Hook and find yourself bundling several related ones together, prefer a thin wrapper over a large, monlithic abstraction. Compare the two component patterns below.
Version 1: All Hooks Inlined
function {
useHook(...);
useHook(...);
useHook(...);
return(
<div>...</div>
);
}
Version 2: Clean Separation
function {
useCustomHook(...);
useHook(...);
useHook(...);
return(
<div>...</div>
);
}
The second version is the better structure. It keeps individual Hooks simple, with remaining useEffect() and useState() calls written inline where they are needed. This makes the logic reusable across components, and it keeps debugging straightforward. Version 1, with many Hooks packed directly into a component, quickly becomes hard to follow.
Organize Before You Optimize
Readable code is a core benefit of Hooks, but a component with many useState() and useEffect() calls can still become messy. Keep the order of your Hooks consistent, and group related logic. If a custom Hook grows too complex, break it down into smaller sub-hooks. Extracting pieces of component logic into dedicated custom Hooks makes the overall flow predictable and easier to trace.
Use Editor Snippets
The React Hooks Snippets extension for Visual Studio Code speeds up writing Hook calls. It currently supports:
useState()useEffect()useContext()useCallback()useMemo()
Install it from the VS Code command palette (Ctrl+P) with the command ext install ALDuncanson.react-hooks-snippets, or search for the extension in the Marketplace. It is a practical aid that pays off during everyday development.
Follow The Rules Of Hooks
The two core rules of Hooks remain the baseline for all code:
- Call Hooks only at the top level of a component, never inside loops, conditions, or nested functions.
- Call Hooks only from React function components or from custom Hooks, not from regular JavaScript functions.
The eslint-plugin-react-hooks plugin enforces both rules automatically and is a sensible addition to any project that uses Hooks.
Because Hooks are still a young API, treat adoption as you would with any early-stage technology: follow the rules, prefer clarity over cleverness, and keep your abstractions minimal. The patterns above — simple Hooks, clean organization, editor tooling, and strict adherence to the rules — will carry most projects forward.



