React Keys Bug — Input Focus Lost on List Reorder
Prepend an item and the text cursor jumps to the wrong input.
20+ years shipping production code across the stack, with years spent interviewing engineers. Drawn from code that ran under real load.
- ✓Solid grasp of fundamentals
- ✓Comfortable reading code examples
- ✓Basic production concepts
- React renders UI declaratively: you describe state, React handles the DOM
- Reconciliation diffs the virtual DOM in O(n) using heuristics (element type + key)
- Hooks rules ensure consistent execution order across renders
- Controlled components store form state in React, uncontrolled in DOM refs
- Performance trap: inline arrow functions break memoisation via useCallback/React.memo
This article dissects the specific React interview questions that separate engineers who've merely read the docs from those who've debugged production apps at 2 AM. It's not a list of trivia — it's a field guide to the five topics that actually matter when you're hiring someone to build and maintain a real React application at scale.
The focus is on the underlying mechanisms (virtual DOM diffing, reconciliation, closure traps in hooks) rather than surface-level syntax, because the interview isn't about whether someone can write a component — it's about whether they understand why it breaks and how to fix it without rewriting the whole thing.
The questions covered here target the most common failure modes in production React: lost input focus during list reordering (the keys bug), stale closures in useEffect, unnecessary re-renders from misuse of useCallback/useMemo, and the controlled-vs-uncontrolled boundary that silently corrupts form state. These aren't academic — they're the bugs that slip through code reviews and cause real user data loss or performance degradation.
The article explains each through the lens of React's actual execution model, not memorized answers.
If you're preparing for a senior React role, this is the material that distinguishes you from someone who can build a todo app. If you're interviewing, these are the questions that expose whether a candidate has felt the pain of debugging a re-render loop or just read about it. The article assumes you already know JSX and state — it's about the hard parts that don't fit in a tweet.
Think of React like a smart whiteboard in a classroom. Instead of erasing and redrawing everything every time something changes, it only erases and redraws the parts that actually changed. That's React's whole superpower — it's incredibly efficient about what it updates on screen. The 'rules' React has (like hooks rules and component structure) are just the whiteboard's instructions for how to keep track of what changed and why.
| Chrome | Firefox | Safari | Edge |
|---|---|---|---|
| ✓ | ✓ | ✓ | ✓ |
React is the most popular front-end library in the world right now, and that means every JavaScript developer eventually walks into an interview room and faces React questions. The problem isn't that these questions are hard — it's that most developers know HOW React works but can't explain WHY it works that way. Interviewers can smell the difference in about 30 seconds. If you can only recite syntax, you'll get filtered out. If you can explain the reasoning behind design decisions, you get the offer.
React solves the messy problem of keeping your UI in sync with your data. Before React, developers manually poked at the DOM and prayed nothing got out of sync. React introduced a declarative model: you describe what the UI should look like given your current state, and React figures out the most efficient way to make it happen. It's the difference between telling someone 'make the button red' versus giving them step-by-step painting instructions every single time.
By the end of this article you'll be able to answer questions about the virtual DOM, reconciliation, hooks rules, controlled vs uncontrolled components, and performance optimization — and more importantly, you'll understand the reasoning deeply enough to handle follow-up questions you've never seen before. That's what actually passes interviews.
What React Interview Questions Actually Test
React interview questions assess your understanding of React's core mechanics: component lifecycle, state management, and the virtual DOM diffing algorithm. They probe beyond syntax into how React reconciles UI updates efficiently. The key mechanic is the reconciliation process — when state or props change, React builds a new virtual DOM tree and diffs it against the previous one to determine minimal real DOM mutations. This is O(n) for typical trees due to heuristics like keyed list comparison.
In practice, the most critical property is the key prop on list elements. Keys allow React to match children across renders, preserving component identity and state. Without stable keys, React defaults to index-based matching, which breaks on reorder, insertion, or deletion — causing unnecessary unmounts and remounts. This is why input focus loss occurs: the component instance is destroyed and recreated, losing its internal state like cursor position.
Use stable, unique keys (like database IDs) for any dynamic list. This matters in real systems because it prevents subtle bugs in forms, drag-and-drop interfaces, and real-time feeds. A senior engineer must understand that keys are not just for performance — they are correctness requirements for component identity.
The Virtual DOM and Reconciliation: The Engine Under the Hood
The most common React interview question is 'What is the Virtual DOM?' but the follow-up 'How does reconciliation work?' is where most candidates stumble. React doesn't just refresh the page; it maintains a lightweight copy of the real DOM in memory. When state changes, React creates a new virtual tree and compares it (diffing) with the previous one.
Interviewers look for an understanding of the O(n) heuristic algorithm React uses. It assumes that two elements of different types will produce different trees and that a 'key' prop helps identify which elements are stable across renders. This prevents unnecessary re-renders and keep the UI snappy.
import React, { useState } from 'react'; /** * TheCodeForge — Reconciliation & Key Demo * This illustrates why 'keys' matter in the Virtual DOM. */ const ListManager = () => { const [items, setItems] = useState(['Docker', 'Kubernetes', 'Spring Boot']); const addTech = () => { // Adding to the start of the array to test React's diffing setItems(['React', ...items]); }; return ( <div className="p-4"> <button onClick={addTech} className="bg-blue-500 text-white px-4 py-2 rounded" > Add React to Stack </button> <ul className="mt-4"> {items.map((item, index) => ( // BAD: Using index as key. React might lose state if items shift. // GOOD: Using the item string (or a unique ID) as the key. <li key={item} className="border-b py-1"> {item} </li> ))} </ul> </div> ); }; export default ListManager;
Hooks Rules: Why They Exist and What Breaks When You Break Them
React hooks enforce two rules: only call hooks at the top level, and only call them from React functions (components or custom hooks). The 'why' is critical for interviews. Under the hood, React relies on the order of hook calls between renders to maintain state and effect references. If you conditionally call a hook, the number of hooks changes between renders, and React's internal list of hook states gets misaligned — leading to bugs like stale state or skipped effects.
Mid-level developers can recite the rules; senior developers explain the linked-list based hook storage mechanism. React stores hooks for a component as a linked list where each hook's pointer depends on the call order. Conditional calls break the order, corrupting the list.
import React, { useState, useEffect } from 'react'; /** * TheCodeForge — Hook Rules Violation Demo * This demonstrates what happens when you break the rules. */ const CounterWithCondition = ({ initial }) => { const [count, setCount] = useState(initial); // BAD: conditionally calling hook if (count > 5) { const [flag, setFlag] = useState(false); // WRONG — violates top-level rule } useEffect(() => { document.title = `Count: ${count}`; }, [count]); return ( <div> <p>Count: {count}</p> <button onClick={() => setCount(c => c + 1)}>Increment</button> </div> ); }; export default CounterWithCondition; // Error: Hooks must not be called conditionally
Controlled vs Uncontrolled Components: The Production Reality
In interviews, you'll be asked the difference between controlled and uncontrolled components. Controlled components keep form state in React state (single source of truth). Uncontrolled components store form data in the DOM itself, and you access it via refs when needed (e.g., form submission). The choice matters for validation, real-time UI updates, and testability.
Most mid-level devs can define both. Senior devs discuss the tradeoffs: controlled components give you full control but cause a re-render on every keystroke (fine for most forms). Uncontrolled components are lighter but harder to react to changes. In production, you'll use controlled for most inputs, uncontrolled for file inputs or when you need to integrate with non-React code.
import React, { useState, useRef } from 'react'; /** * TheCodeForge — Controlled vs Uncontrolled Example */ const FormExample = () => { // Controlled input with state const [email, setEmail] = useState(''); // Uncontrolled input with ref const passwordRef = useRef(null); const handleSubmit = (e) => { e.preventDefault(); const password = passwordRef.current.value; console.log('Controlled email:', email); console.log('Uncontrolled password:', password); }; return ( <form onSubmit={handleSubmit}> <div> <label>Email (Controlled):</label> <input type="email" value={email} onChange={(e) => setEmail(e.target.value)} /> </div> <div> <label>Password (Uncontrolled):</label> <input type="password" ref={passwordRef} defaultValue="" /> </div> <button type="submit">Submit</button> </form> ); }; export default FormExample;
- Controlled: value + onChange === React is the sole source of truth.
- Uncontrolled: defaultValue + ref === DOM holds the current value, React only reads when needed.
- When you need to react to every change (e.g., live validation) → controlled.
- When you only need the value at submit (e.g., password) → uncontrolled is simpler.
- File inputs are always uncontrolled — you can't set the file path via state.
useEffect and Lifecycle: The Cleanup Trap
useEffect is the gateway to side effects in React components. It replaces componentDidMount, componentDidUpdate, and componentWillUnmount — but with a functional twist. The function passed to useEffect runs after every render by default, unless you specify a dependency array. The cleanup function returned from the effect runs before the effect re-runs and on unmount.
Interviewers probe for understanding of the dependency array and cleanup. Common pitfalls: missing dependencies causing stale closures, omitting cleanup leading to memory leaks (e.g., subscriptions or timers).
import React, { useState, useEffect } from 'react'; /** * TheCodeForge — useEffect with Cleanup * Tracks online/offline status using a browser event. */ const OnlineStatus = () => { const [isOnline, setIsOnline] = useState(navigator.onLine); useEffect(() => { const handleOnline = () => setIsOnline(true); const handleOffline = () => setIsOnline(false); window.addEventListener('online', handleOnline); window.addEventListener('offline', handleOffline); // Cleanup function: removes event listeners to prevent memory leak return () => { window.removeEventListener('online', handleOnline); window.removeEventListener('offline', handleOffline); }; }, []); // Empty array: effect runs once (mount) and cleanup on unmount return ( <div> <p>You are currently: {isOnline ? 'Online' : 'Offline'}</p> </div> ); }; export default OnlineStatus;
Performance Optimisation: memo, useMemo, useCallback
React's reconciliation is fast, but unnecessary re-renders drag down performance in complex apps. The interview topic: when and how to prevent wasteful re-renders. React.memo wraps a component to skip re-render if props haven't changed (shallow comparison). useMemo memoises expensive computation results. useCallback memoises callback functions to maintain referential stability across renders.
Interviewers like to see that you understand when NOT to use these tools. Over-optimisation can make code harder to debug and even hurt performance by holding onto large memoised objects. The key is: only memoise when you've measured a re-render bottleneck.
import React, { useState, useMemo, useCallback } from 'react'; /** * TheCodeForge — Memoisation Patterns * Optimising a component that renders a large filtered list. */ const ExpensiveList = React.memo(({ items, onToggle }) => { console.log('ExpensiveList rendered'); return ( <ul> {items.map(item => ( <li key={item.id} onClick={() => onToggle(item.id)}> {item.name} </li> ))} </ul> ); }); const App = () => { const [filter, setFilter] = useState(''); const [items] = useState([/* large dataset */]); // useMemo: avoid recomputing filtered list on every render const filteredItems = useMemo( () => items.filter(item => item.name.includes(filter)), [items, filter] ); // useCallback: stable reference for the callback so React.memo can work const handleToggle = useCallback((id) => { console.log('Toggled item', id); }, []); return ( <div> <input placeholder="Filter" value={filter} onChange={(e) => setFilter(e.target.value)} /> <ExpensiveList items={filteredItems} onToggle={handleToggle} /> </div> ); }; export default App;
How Does React Actually Work? (The Bits That Matter in Production)
Interviews love this question because it's the difference between someone who read a tutorial and someone who's debugged a render loop at 2 AM.
React works because it doesn't touch the real DOM until it absolutely has to. Your components return JSX, which Babel compiles into React.createElement() calls. Those create a tree of plain JavaScript objects — the Virtual DOM. When state changes, React builds a new tree, diffs it against the previous one (reconciliation), and computes the minimal set of DOM mutations.
The key insight: reconciliation isn't free. The algorithm is O(n) based on heuristics — same position in the tree? Reuse. Different type? Tear down and rebuild. That's why key props matter. Slap a random index on a list and React will re-render every child when you delete the first item. Use a stable ID and it skips the entire subtree.
One-way data flow isn't a philosophy — it's a constraint that makes your life easier. Data goes down via props, events come back up via callbacks. No magic two-way binding, no surprise mutations. Predictable state changes mean predictable bugs.
// io.thecodeforge — interview tutorial // Bad: index as key re-renders everything on reorder function OrderList({ orders }) { return orders.map((order, idx) => ( <OrderCard key={idx} order={order} /> )); } // Good: stable ID only re-renders changed items function OrderList({ orders }) { return orders.map(order => ( <OrderCard key={order.transactionId} order={order} /> )); }
index as key in a sortable/filterable list will cause stale state bugs and unnecessary re-renders. Always use unique, stable identifiers.JSX: The Syntax Your Interviewer Expects You to Actually Understand
JSX isn't HTML. It's syntactic sugar for React.createElement(type, props, ...children). Every interview question about JSX is really asking: do you understand the compilation step, or do you think React runs raw HTML?
When you write <div className="card">Hello</div>, Babel turns it into: React.createElement('div', { className: 'card' }, 'Hello')
That className is a dead giveaway — JSX uses the DOM API property names, not HTML attributes. htmlFor instead of for, tabIndex instead of tabindex. These catch people who never read the compiled output.
The {} for JavaScript expressions is because JSX is just function calls. Anything inside those braces must be an expression — not a statement. You can't write if inside JSX. You use ternary or logical &&. That's not a design choice, it's the compiler screaming at you.
Strings in JSX are auto-escaped against XSS. Raw HTML requires dangerouslySetInnerHTML — and yes, the name is intentionally ugly. Don't use it unless you've sanitized the input server-side.
// io.thecodeforge — interview tutorial // What you write function Greeting({ user }) { return <h1 id="title">Welcome, {user.name}</h1>; } // What Babel compiles it to function Greeting({ user }) { return React.createElement( 'h1', { id: 'title' }, 'Welcome, ', user.name ); }
class → className, for → htmlFor, tabindex → tabIndex. Interviewers watch for these slip-ups.Components: Functions That Return UI (And Nothing Else)
A React component is a function that takes props and returns JSX. That's it. The entire "component architecture" hype boils down to: compose small functions into larger ones. Class components were the old way — stateful, lifecycle-heavy, and verbose. Functional components with hooks are the present.
The difference matters in interviews. Class components have this, lifecycle methods, and more boilerplate. Functional components have hooks and no this confusion. But the real question is: why do functional components outperform class components in production?
Functional components are lighter. No this binding overhead. No instance creation. Hooks let you colocate state and effects without scattering logic across lifecycle methods. A class component needs componentDidMount, componentDidUpdate, and componentWillUnmount to handle side effects. A functional component uses one useEffect with a cleanup function.
Interviewers ask this to see if you've shipped both patterns. Say you prefer functional — then explain why: less code, easier testing, better tree-shaking. Class components aren't going anywhere in legacy codebases, but you wouldn't start a new one with them.
// io.thecodeforge — interview tutorial // Class component (legacy) class UserProfile extends React.Component { componentDidMount() { fetchUser(this.props.userId); } render() { return <div>{this.props.name}</div>; } } // Functional component (modern) function UserProfile({ userId, name }) { useEffect(() => { fetchUser(userId); }, [userId]); return <div>{name}</div>; }
When should you use Redux over Context API?
Redux and Context API both solve state sharing. They are not interchangeable. Context is a dependency injection mechanism, not a state manager. When a value changes, every consumer re-renders. That's fine for theme or locale. It's a disaster for a shopping cart with 500 items.
Redux gives you granular subscriptions. Only components that depend on specific slices of state re-render. It also enforces a unidirectional data flow with reducers, which makes debugging and testing predictable. You don't reach for Redux because you have global state. You reach for Redux because you have complex state transitions, middleware needs, or performance requirements that Context can't meet.
Production rule: if your state changes less than once per second and affects fewer than 10 components, Context is fine. If those numbers climb, or if you need time-travel debugging, pick Redux.
// io.thecodeforge — interview tutorial // All 500 product cards re-render every time cart changes const CartContext = React.createContext(); function CartProvider({ children }) { const [cart, setCart] = useState({items: [], total: 0}); return ( <CartContext.Provider value={{cart, setCart}}> {children} {/* Every consumer re-renders */} </CartContext.Provider> ); } // Fix: Redux useSelector subscribes to cart.items only // const items = useSelector(state => state.cart.items); // Components that read only `total` won't re-render
How would you optimize a slow React application?
Before touching a single line of code, profile. Use React DevTools Profiler and browser Performance tab. Find the bottleneck. Is it mounting, re-rendering, or network? The answer changes your strategy.
If re-renders are the culprit, start with React.memo on pure presentational components. Then check if useCallback and useMemo actually help — they add overhead. Only use them when you pass props to memoized children or when a calculation costs more than a render.
For list-heavy UIs, virtualize. react-window or react-virtuoso render only what's visible. For large forms, isolate field state so one keystroke doesn't re-render the entire form. For images, lazy load and use modern formats like WebP.
Network optimization is worth more than micro-optimizations: code-split with React.lazy and Suspense, compress assets, cache API responses. The fastest render is the one you skip entirely.
// io.thecodeforge — interview tutorial // BEFORE: Every search input keypress re-renders 200 rows function SearchPage() { const [query, setQuery] = useState(''); const results = useExpensiveSearch(query); // Runs every render return <ResultsList items={results} />; } // AFTER: Debounce + memoized component function SearchPage() { const [query, setQuery] = useState(''); const debouncedQuery = useDebounce(query, 300); const results = useMemo(() => expensiveSearch(debouncedQuery), [debouncedQuery]); return <MemoizedResultsList items={results} />; }
List Reordering Broke Input Focus: The Hidden Key Bug
- Never use array index as key if the list order can change (insert, delete, sort).
- Stable ID-based keys preserve component state across re-renders.
- React's reconciliation relies on keys to match previous component instances to current data.
console.log('key:', key) in render to see current keysgit grep 'key={index}' to find all index-based keysconsole.log('deps:', deps) to log dependency array each renderCheck if state setter is inside the effect without depsUse React DevTools Profiler to see why component re-renderedconsole.log('props changed', newProps, prevProps) in custom comparison| Feature | Virtual DOM | Real DOM |
|---|---|---|
| Update Speed | Blazing fast (JavaScript objects) | Slow (triggers browser layout/reflow) |
| Memory Usage | Low (only in-memory objects) | High (complex browser structures) |
| Efficiency | Batched updates via Reconciliation | Individual, manual manipulation |
| Consistency | Declarative (UI follows State) | Imperative (UI must be manually poked) |
| File | Command / Code | Purpose |
|---|---|---|
| io | /** | The Virtual DOM and Reconciliation |
| io | /** | Hooks Rules |
| io | /** | Controlled vs Uncontrolled Components |
| io | /** | useEffect and Lifecycle |
| io | /** | Performance Optimisation |
| ReconciliationPitfall.py | function OrderList({ orders }) { | How Does React Actually Work? (The Bits That Matter in Produ |
| JSXCompilation.py | function Greeting({ user }) { | JSX |
| ClassVsFunction.py | class UserProfile extends React.Component { | Components |
| WhenContextHurts.tsx | const CartContext = React.createContext(); | When should you use Redux over Context API? |
| ProfileFirst.tsx | function SearchPage() { | How would you optimize a slow React application? |
Key takeaways
Common mistakes to avoid
5 patternsUsing array index as a key prop
Mutating state directly instead of using setState
Omitting cleanup in useEffect for subscriptions and timers
Confusing props and state usage
Calling hooks conditionally or inside loops
Interview Questions on This Topic
What is the virtual DOM and how does it differ from the real DOM?
Explain the Rules of Hooks. Why does React enforce calling hooks only at the top level and only from React functions?
What is the difference between controlled and uncontrolled components? When would you use each?
How does React.memo differ from useMemo and useCallback?
What happens if you use an empty dependency array in useEffect but access a reactive value inside it?
Frequently Asked Questions
A Controlled component has its value managed by React state; the single source of truth is the state variable. In an Uncontrolled component, the data is handled by the DOM itself, usually accessed via a Ref. Use controlled components for validation and real-time UI updates.
useEffect is a combined replacement for componentDidMount, componentDidUpdate, and componentWillUnmount. An empty dependency array [] mimics 'mount', an array with variables [data] mimics 'update' for those specific items, and returning a function from the effect handles the 'unmount' or cleanup phase.
Lifting state up is the pattern of moving state from a child component to its closest common ancestor when multiple child components need to share or synchronize that data. This maintains the 'unidirectional data flow' principle of React.
The key prop helps React identify which items have changed, been added, or been removed during reconciliation. It should be a stable and unique identifier for each item (e.g., item ID). Using array index as key is dangerous when the list order can change.
Use useReducer when you have complex state logic involving multiple sub-values, or when the next state depends on the previous state in a non-trivial way. It's also useful when you want to decouple state update logic from the component for testability.
20+ years shipping production code across the stack, with years spent interviewing engineers. Drawn from code that ran under real load.
That's JavaScript Interview. Mark it forged?
6 min read · try the examples if you haven't