React Hooks Masterclass: useState, useEffect, and Custom Hooks

How does React remember information between renders?
A regular JavaScript function forgets everything the moment it returns. Call it again, and every local variable starts fresh. But a React component clearly doesn’t forget — a counter keeps counting, a form keeps its typed text, a toggle stays toggled, across dozens of re-renders.
Hooks are the mechanism that makes this possible — React’s way of letting a function component hold onto memory (useState) and reach outside itself to synchronize with the world (useEffect), without ever needing to become a class. This guide builds both from first principles, then shows how to package reusable logic into your own custom hooks.
1. Why React Hooks Were Introduced
Problems with older React patterns
Before Hooks, stateful logic required class components — more verbose syntax, a confusing this binding, and lifecycle methods (componentDidMount, componentDidUpdate, componentWillUnmount) that often forced related logic (like setting up and tearing down a subscription) to be split awkwardly across separate methods.
Reusing logic between components
Sharing stateful logic between class components typically meant patterns like higher-order components or render props — functional, but often adding extra wrapper layers and indirection just to reuse a small piece of behavior.
Simpler component development
Hooks let function components — already simpler and more readable than classes — hold state and perform side effects directly, removing the need to reach for a class at all for the vast majority of components.
Benefits of hooks
- Related logic (setup and cleanup for the same concern) can live together in one place, instead of split across separate lifecycle methods
- Reusable stateful logic can be extracted into plain functions (custom hooks) — no wrapper components, no indirection
- Simpler mental model overall: components are just functions, enhanced with a few special capabilities
Modern React development
Hooks are now the default way to write React — new codebases are built almost entirely with function components and hooks, with class components mostly encountered only in older, legacy code.

2. Understanding useState
What useState is
useState is a hook that gives a function component a piece of memory that persists across re-renders — something a plain function normally can't do.
Creating component state
const [count, setCount] = useState(0);This returns a pair: the current value (count), and a function to update it (setCount). The 0 passed in is the initial value, used only on the very first render.
Updating state
<button onClick={() => setCount(count + 1)}>Increment</button>Calling setCount doesn't just change a variable — it tells React "this component's data changed, please re-render it with the new value."
Multiple state variables
A component can hold as many independent useState calls as it needs:
function ProfileForm() {
const [name, setName] = useState("");
const [email, setEmail] = useState("");
const [isEditing, setIsEditing] = useState(false);
// ...
}Each behaves independently — updating one doesn’t affect the others.
State-driven UI
The UI a component renders should always be a direct reflection of its current state — as covered in this series’s React Fundamentals article, this is the core of React’s declarative model.
Common state management patterns
- Boolean state for toggles (
isOpen,isLoading) - Object or array state for more complex, grouped data
- Deriving values from existing state during render, rather than storing every derived value as its own separate state

3. Understanding React Re-renders
What triggers a re-render
A component re-renders when its own state changes, when its props change, or when its parent re-renders — the same model covered in this series’s state-management article, worth restating here since it’s the direct consequence of every useState call.
State updates and rendering
Every call to a state setter schedules a re-render of that component (and, by default, its children) — this is the trigger that turns “data changed” into “UI updated.”
How React updates the UI
Behind every re-render sits the Virtual DOM and reconciliation process covered in this series’s dedicated internals article — React re-runs the component, builds a new Virtual DOM tree, and applies only the real changes needed.
Avoiding unnecessary state
Not every value needs its own useState. A value that can be directly computed from existing state or props during render doesn't need to be duplicated into its own state variable — doing so risks the copy silently drifting out of sync with its source.
Common misconceptions
A common misunderstanding: that calling a state setter updates the DOM immediately, synchronously. In reality, React schedules the update and re-renders on its own timing — code immediately after a setState call still sees the old value in that same render.
function handleClick() {
console.log(count); // still the old value here
setCount(count + 1);
console.log(count); // still the old value here too — the update hasn't applied yet
}




