React Hooks – Complete Guide (With Full Definitions & Examples)
When React 16.8 introduced Hooks, they completely changed the way developers write components. Before hooks, if you wanted to use state or lifecycle methods, you had no choice but to write class components. Now, with hooks, you can manage state, side effects, refs, context, and even build your own custom logic inside functional components.
Rules of Hooks
Definition:
The rules of hooks are two principles that ensure hooks work predictably.
Explanation:
React relies on the order of hook calls to know which state or effect belongs to which component. If you break these rules, React can’t match hooks to the right state.
1. Call hooks only at the top-level
Don’t call hooks inside loops, conditions, or nested functions.
Always call them in the same order.
✅ Good:
function App() {
const [count, setCount] = useState(0); // always at top
return <p>{count}</p>;
}
❌ Bad:
function App() {
if (true) {
const [count, setCount] = useState(0); // breaks the rules
}
}
2. Call hooks only inside React functions
Hooks must be used inside functional components or custom hooks. Never call them in normal JS functions.
useState
Definition:
useState is a hook that lets you add state variables to functional components.
Explanation:
State is data that changes over time (like a counter, input value, or toggle). Every time state updates, the component re-renders to reflect the new value.
Example – Simple counter
import { useState } from "react";
function Counter() {
const [count, setCount] = useState(0); // state variable with default value 0
return (
<button onClick={() => setCount(count + 1)}>
Count: {count}
</button>
);
}
Lazy initialization
Definition: Instead of passing a value, you can pass a function that computes the initial state.
Explanation: This is useful when the initial value is expensive to calculate. React will only run this function once (when the component mounts).
function ExpensiveCounter() {
const [count, setCount] = useState(() => {
console.log("Initial calculation runs once");
return 0;
});
return <button onClick={() => setCount(c => c + 1)}>Count: {count}</button>;
}
Function vs direct value updates
Direct value: setCount(5) replaces state with 5.
Function form: setCount(prev => prev + 1) updates based on the previous value (recommended).
useEffect
Definition:
useEffect lets you perform side effects in your component.
Explanation:
Side effects are actions that affect something outside the component (fetching data, subscriptions, timers, logging). By default, effects run after every render.
Example – Fetching data
import { useEffect, useState } from "react";
function Users() {
const [users, setUsers] = useState([]);
useEffect(() => {
async function fetchUsers() {
const res = await fetch("https://jsonplaceholder.typicode.com/users");
setUsers(await res.json());
}
fetchUsers();
}, []); // empty array = run only once after mount
return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}
Pitfalls
1. Infinite loops
If you forget dependencies or update state directly inside the effect, it may re-run forever.
❌ Bad:
useEffect(() => {
setCount(count + 1); // triggers re-render → infinite loop
});
2. Cleanup function
Effects can return a cleanup function that runs before the effect runs again or before the component unmounts.
useEffect(() => {
const id = setInterval(() => console.log("tick"), 1000);
return () => clearInterval(id); // cleanup
}, []);
3. Async effects
You cannot make the effect function async. Instead, define an async function inside.
useLayoutEffect vs useEffect
Definition:
Both hooks let you run side effects, but at different times.
Explanation:
useEffect runs after the browser paints.
useLayoutEffect runs synchronously after DOM mutations but before paint.
👉 Use useLayoutEffect when you need to measure DOM elements or make synchronous layout changes.
Example:
import { useRef, useLayoutEffect } from "react";
function Box() {
const ref = useRef();
useLayoutEffect(() => {
console.log("Box width:", ref.current.offsetWidth);
}, []);
return <div ref={ref} style={{ width: "200px" }}>Box</div>;
}
useImperativeHandle with forwardRef
Definition:
useImperativeHandle customizes the value exposed to a parent when using ref.
Explanation:
Normally, refs expose DOM elements. With useImperativeHandle, you can expose specific methods (like .focus()) from child to parent.
import { useRef, forwardRef, useImperativeHandle } from "react";
const Input = forwardRef((props, ref) => {
const inputRef = useRef();
useImperativeHandle(ref, () => ({
focus: () => inputRef.current.focus()
}));
return <input ref={inputRef} />;
});
function App() {
const inputRef = useRef();
return (
<>
<Input ref={inputRef} />
<button onClick={() => inputRef.current.focus()}>Focus input</button>
</>
);
}
useId (React 18)
Definition:
Generates a unique, stable ID for components.
Explanation:
Useful for accessibility when linking labels and inputs, ensuring IDs don’t collide on the page.
import { useId } from "react";
function FormField() {
const id = useId();
return (
<>
<label htmlFor={id}>Name</label>
<input id={id} type="text" />
</>
);
}
useDebugValue
Definition:
useDebugValue helps you label custom hooks for easier debugging in React DevTools.
Explanation:
It doesn’t affect the app but improves developer experience.
import { useState, useDebugValue } from "react";
function useOnlineStatus() {
const [online] = useState(navigator.onLine);
useDebugValue(online ? "Online" : "Offline");
return online;
}
useReducer + useContext
Definition:
useReducer manages complex state with reducer functions.
useContext shares data globally without prop drilling.
Explanation:
Together, they can replace Redux for small to medium apps.
import { useReducer, createContext, useContext } from "react";
const StateContext = createContext();
function reducer(state, action) {
switch (action.type) {
case "increment": return { count: state.count + 1 };
default: return state;
}
}
export function Provider({ children }) {
const [state, dispatch] = useReducer(reducer, { count: 0 });
return (
<StateContext.Provider value={{ state, dispatch }}>
{children}
</StateContext.Provider>
);
}
function Counter() {
const { state, dispatch } = useContext(StateContext);
return (
<>
<p>{state.count}</p>
<button onClick={() => dispatch({ type: "increment" })}>+</button>
</>
);
}
useSyncExternalStore (React 18)
Definition:
A hook for reading external stores (like Redux or browser APIs) in a way that works with concurrent rendering.
Explanation:
Ensures consistency between store values and React’s rendering.
import { useSyncExternalStore } from "react";
function usePathname() {
return useSyncExternalStore(
(listener) => {
window.addEventListener("popstate", listener);
return () => window.removeEventListener("popstate", listener);
},
() => window.location.pathname
);
}
function PathDisplay() {
const path = usePathname();
return <p>Current path: {path}</p>;
}
useInsertionEffect (React 18)
Definition:
Runs before DOM mutations, mainly for injecting styles in CSS-in-JS libraries.
Explanation:
Rarely needed in normal apps but essential for styling libraries.
import { useInsertionEffect } from "react";
function StyledBox() {
useInsertionEffect(() => {
const style = document.createElement("style");
style.innerHTML = ".box { color: red }";
document.head.appendChild(style);
return () => style.remove();
}, []);
return <div className="box">Styled Box</div>;
}
Recap
We covered all important hooks with definitions, explanations, and examples:
Rules of hooks
useState (lazy init, functional updates)
useEffect (cleanup, async, pitfalls)
useLayoutEffect vs useEffect
useImperativeHandle with forwardRef
useId, useDebugValue
Global state with useReducer + useContext
Advanced React 18 hooks: useSyncExternalStore, useInsertionEffect
Final Thoughts
Hooks make React more powerful, but also more elegant. Once you truly understand them, you can build scalable, reusable, and modern React apps without needing extra libraries for many use cases.
Comments
Post a Comment