Welcome to Part 2 of our React interview series. In the previous lesson, we covered the building blocks (Components, Props, State). Now, we enter the era of Hooks.
Introduced in React 16.8, Hooks revolutionized how we write React components. They allow functional components to have state and lifecycle features that were previously reserved for class components. If you are coming from Python, think of Hooks as special mixins or decorators that give your simple functions superpowers.
Hooks are functions that let you "hook into" React state and lifecycle features from function components. They always start with the word use (e.g., useState, useEffect).
There are two strictly enforced rules you must mention in an interview:
Analogy: Imagine a coat check at a theater. You give them your coat, and they put it in slot #1. You give them your hat, and they put it in slot #2. When you come back, you must ask for your items in the same order. If you skip slot #1 because of an "if statement," the attendant will give you your hat when you asked for your coat. The order matters!
// ❌ BAD: Conditional Hook
if (userIsLoggedIn) {
const [name, setName] = useState("User"); // React loses track if this condition changes!
}
// ✅ GOOD: Top Level
const [name, setName] = useState("User");
// Use the condition inside the logic, not to define the hook
useEffect(() => {
if (userIsLoggedIn) { /* logic */ }
}, [userIsLoggedIn]);
Both hooks manage state, but they are designed for different levels of complexity.
Analogy:
useState: A light switch. You just flip it On or Off. Simple.
useReducer: An airport traffic controller. You don't just say "move plane." You send specific commands ("Takeoff", "Land", "Taxi") and the controller updates the complex map of the runway accordingly.
// 1. useState (Simple)
const [count, setCount] = useState(0);
// setCount(count + 1);
// 2. useReducer (Complex/Structured)
const initialState = { count: 0 };
function reducer(state, action) {
switch (action.type) {
case 'increment': return { count: state.count + 1 };
case 'decrement': return { count: state.count - 1 };
default: throw new Error();
}
}
const [state, dispatch] = useReducer(reducer, initialState);
// dispatch({ type: 'increment' });
useEffect lets you perform side effects (data fetching, subscriptions, manual DOM changes) in function components. It serves the same purpose as componentDidMount, componentDidUpdate, and componentWillUnmount combined.
| Lifecycle Goal | useEffect Syntax | Trigger |
|---|---|---|
| Mount | useEffect(fn, []) | Runs once on load |
| Update | useEffect(fn, [prop]) | Runs when `prop` changes |
| Unmount | useEffect(() => cleanupFn, []) | Runs cleanup on removal |
useEffect(() => {
console.log("1. Component Mounted (Fetching Data...)");
// Optional: Return a cleanup function (Unmount)
return () => {
console.log("3. Component Unmounted (Cleaning up...)");
};
}, []); // Empty dependency array = Run Once
const [count, setCount] = useState(0);
useEffect(() => {
console.log("2. Count Updated:", count);
}, [count]); // Runs whenever 'count' changes
Sample behavior (Console):
// On Load:
1. Component Mounted (Fetching Data...)
2. Count Updated: 0
// On Button Click (setCount):
2. Count Updated: 1
// On Page Leave:
3. Component Unmounted (Cleaning up...)
Both can store data, but they affect the React render cycle differently.
Analogy:
useState: A public announcement system. When you change the message, everyone stops and listens (Re-render).
useRef: A sticky note in your pocket. You can write on it and read it later, but nobody else knows you changed it, and it doesn't interrupt the show.
const [count, setCount] = useState(0); // Changing this updates the screen
const renderCount = useRef(0); // Changing this happens silently
useEffect(() => {
renderCount.current = renderCount.current + 1;
});
return (
<div>
<h1>State: {count}</h1>
{/* This will show how many times the component rendered,
without causing an infinite loop of renders itself. */}
<h2>Renders: {renderCount.current}</h2>
<button onClick={() => setCount(c => c + 1)}>Update State</button>
</div>
);
A Custom Hook is simply a JavaScript function that starts with "use" and calls other Hooks inside it. It is the primary mechanism for reusing stateful logic across different components.
Before Hooks, we used complex patterns like Higher-Order Components (HOCs) or Render Props to share logic. Custom Hooks make this much cleaner. If two components need to fetch data, toggle a modal, or track window size, you extract that logic into a Custom Hook.
// --- The Custom Hook (useToggle.js) ---
function useToggle(initialValue = false) {
const [value, setValue] = useState(initialValue);
const toggle = () => {
setValue((prev) => !prev);
};
// Return exactly what the component needs
return [value, toggle];
}
// --- Component A ---
const Modal = () => {
const [isOpen, toggleModal] = useToggle(false);
return <button onClick={toggleModal}>{isOpen ? "Close" : "Open"} Modal</button>;
};
// --- Component B ---
const Menu = () => {
const [isExpanded, toggleMenu] = useToggle(true);
return <button onClick={toggleMenu}>{isExpanded ? "Collapse" : "Expand"} Menu</button>;
};
Tip: Always name your custom hooks starting with use (e.g., useFetch, useForm). This tells React (and your teammates) that this function follows the Rules of Hooks.
You have now mastered the core toolset of modern React! Understanding the nuance between useEffect dependency arrays and knowing when to reach for useRef versus useState will solve 90% of the bugs you encounter. In the next lesson, we will tackle State Management and how to pass data around effectively.
2 questions · no sign-up, nothing stored
Progress is stored in this browser only - no sign-up, nothing sent anywhere.