Difficulty: Advanced
This lesson covers the most frequently asked React interview questions. These topics appear in almost every React interview and having crisp, confident answers will set you apart. We cover controlled vs uncontrolled components, class vs function components, the rules of hooks, React's synthetic event system, and Strict Mode.
Controlled components have their form values driven by React state. The input's value is set from state, and onChange updates state. React is the single source of truth. Uncontrolled components let the DOM manage the form state, and you read values with refs. Controlled components give you full control (validation on every keystroke, conditional disabling, formatting), but they require more code. Uncontrolled components are simpler for basic forms and are useful when integrating with non-React code. The default recommendation is controlled components.
Class components and function components can both express any React UI. Class components use this.state, this.setState, and lifecycle methods. Function components use hooks (useState, useEffect, etc.). Function components are now the standard because they are simpler, compose better with hooks, and avoid the confusing 'this' binding issues of classes. Class components are still needed for error boundaries (componentDidCatch has no hook equivalent). In interviews, you should be comfortable with both but explain that function components with hooks are the modern standard.
The rules of hooks exist because hooks rely on the order in which they are called. React tracks hooks by their call order in a linked list. Rule 1: Only call hooks at the top level - never inside loops, conditions, or nested functions, because this would change the call order between renders. Rule 2: Only call hooks from React function components or custom hooks - not from regular JavaScript functions, because React needs to associate the hook with a component instance.
React's synthetic event system wraps native browser events in a cross-browser wrapper called SyntheticEvent. It normalizes event behavior across browsers (consistent properties, consistent bubbling). Events are delegated to the root - React listens at the root DOM node and dispatches to the correct component handler. In React 17+, events are attached to the root container instead of document, improving compatibility with multiple React roots.
// CONTROLLED: React state drives the input
function ControlledForm() {
const [email, setEmail] = React.useState('');
const [error, setError] = React.useState('');
const handleChange = (e) => {
const val = e.target.value;
setEmail(val);
setError(val.includes('@') ? '' : 'Invalid email');
};
return (
<div>
<input value={email} onChange={handleChange} />
{error && <span className="text-red-600">{error}</span>}
</div>
);
}
// UNCONTROLLED: DOM manages the value, read with ref
function UncontrolledForm() {
const emailRef = React.useRef();
const handleSubmit = (e) => {
e.preventDefault();
console.log('Email:', emailRef.current.value);
};
return (
<form onSubmit={handleSubmit}>
<input ref={emailRef} defaultValue="" />
<button type="submit">Submit</button>
</form>
);
}
Controlled: value + onChange. You can validate on every keystroke. Uncontrolled: defaultValue + ref. You read the value on submit. Controlled is preferred for most cases because it gives React full control over the form state.
// CORRECT: hooks always called in the same order
function UserProfile({ userId }) {
const [user, setUser] = React.useState(null); // Hook 1
const [loading, setLoading] = React.useState(true); // Hook 2
React.useEffect(() => { // Hook 3
fetchUser(userId).then(setUser);
}, [userId]);
return loading ? <p>Loading...</p> : <h1>{user.name}</h1>;
}
// WRONG: hook inside a condition - order changes between renders!
function BadComponent({ showExtra }) {
const [name, setName] = React.useState(''); // Hook 1
if (showExtra) {
// This hook only runs sometimes - React loses track!
const [extra, setExtra] = React.useState(''); // Sometimes Hook 2
}
// React thinks this is Hook 2, but it might be Hook 3
React.useEffect(() => {}, []); // Hook 2 or 3??
}
React stores hooks in a linked list indexed by call order. If a hook is conditionally skipped, all subsequent hooks shift by one, causing React to read the wrong state for each hook.
import React from 'react';
import ReactDOM from 'react-dom/client';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
// StrictMode in development:
// 1. Double-invokes render functions (to detect impure renders)
// 2. Double-invokes effects (mount -> unmount -> mount) to find missing cleanups
// 3. Warns about deprecated APIs (findDOMNode, legacy context, string refs)
// 4. No effect in production - zero runtime cost
// Example: effect runs twice in dev
function MyComponent() {
React.useEffect(() => {
console.log('Effect ran'); // Logs twice in dev, once in prod
return () => console.log('Cleanup'); // Also runs in between
}, []);
}
Strict Mode intentionally double-renders and double-fires effects in development to help you find bugs: impure render functions, missing cleanup, and side effects in render. It has zero cost in production.
controlled vs uncontrolled, class vs function components, rules of hooks, synthetic events, strict mode