💡 Hint: Declarative UI library — describe what the UI should look like, not how to update it
React is a JavaScript library for building user interfaces. It solves the problem of keeping the UI in sync with application state.
Core ideas:
Declarative — you describe the desired UI for a given state; React figures out DOM updates
Component-based — split UI into reusable, self-contained pieces
Virtual DOM — React maintains a lightweight copy of the DOM, diffs it on state change, and batches minimal real DOM updates
Unidirectional data flow — data flows down (props), events flow up (callbacks)
// Imperative (jQuery style) — how to update
const btn = document.getElementById('btn');
btn.addEventListener('click', () => {
const count = parseInt(btn.textContent) + 1;
btn.textContent = count;
});
// Declarative (React style) — what it should look like
function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}
💡 React's real innovation was making UI a pure function of state: UI = f(state). Any time state changes, React re-runs the function and reconciles the difference.
💡 Hint: JSX is syntactic sugar — Babel transforms it into React.createElement() calls
JSX is a syntax extension that looks like HTML inside JavaScript. It is NOT valid JS — it gets compiled by Babel into React.createElement() calls.
// What you write
const el =
Hello {name}
;
// What Babel compiles it to
const el = React.createElement('h1', { className: 'title' }, 'Hello ', name);
// React 17+ — new JSX transform (no need to import React)
import { jsx as _jsx } from 'react/jsx-runtime';
const el = _jsx('h1', { className: 'title', children: ['Hello ', name] });
Key rules:
Single root element required (use <></> Fragment if needed)
Use className not class, htmlFor not for
Expressions in {} — not statements (if, for)
Self-close empty tags: <img />, <br />
camelCase for attributes: onClick, onChange
💡 JSX returns plain JS objects (React elements). They're just descriptions — cheap to create. React uses these to build and diff the Virtual DOM.
What is the Virtual DOM and how does reconciliation work?
💡 Hint: Lightweight JS copy of the DOM — React diffs old vs new tree, applies minimal patches
The Virtual DOM is a JavaScript object tree that mirrors the real DOM. It's cheap to create and manipulate.
Reconciliation process:
State/props change triggers a re-render — React calls your component function
React builds a new Virtual DOM tree
Diffing — React compares (diffs) new tree with the previous tree
Committing — React applies only the changed nodes to the real DOM
Diffing heuristics (O(n) instead of O(n³)):
Elements of different types → tear down and rebuild the whole subtree
Same type → update only changed attributes
Lists → use key prop to match old and new items
// React sees these as DIFFERENT types → full rebuild
...
→ ... // destroys div, creates span
// Same type → just updates className attribute
→
💡 Reconciliation is why key matters so much in lists. Without a stable key, React might reuse the wrong DOM node, causing subtle bugs like input state being wrong item.
Uncontrolled — file inputs (always uncontrolled), integrating with non-React code, simple one-off forms where you only need value on submit
💡 React recommends controlled components for most cases. File inputs (<input type="file">) are always uncontrolled — you can't set their value programmatically for security reasons.
Prop drilling — passing props through intermediate components that don't need them, just so a deep child can access them:
// ❌ Drilling — Middle doesn't need user, just passes it down
function App() {
const user = { name: 'Alice' };
return ;
}
function Middle({ user }) {
return ;
}
function Deep({ user }) {
return
{user.name}
;
}
Solutions to prop drilling:
Context API — broadcast value to any descendant
Component composition — lift rendering up, pass JSX as children or slots
State management — Zustand, Redux, Jotai for global state
💡 Before reaching for Context, try composition first. Pass children or render props — often eliminates drilling without the overhead of a context.
What is the key prop and why is it critical in lists?
💡 Hint: Key lets React identify which item changed, was added, or removed — must be stable and unique
The key prop is React's mechanism to identify list items across renders. Without stable keys, React can't efficiently reconcile list changes.
// ❌ Using index as key — problematic when list reorders or items are inserted
{items.map((item, index) => (
))}
// ✅ Use a stable unique ID from the data
{items.map(item => (
))}
Why index keys fail:
// Initial: [A(key=0), B(key=1), C(key=2)]
// Delete A: [B(key=0), C(key=1)]
// React sees key=0 CHANGED (not deleted) → updates B in place
// Key=2 REMOVED → destroys C
// Result: input state, focus, animations on wrong elements
When index keys are safe:
The list never reorders
Items are never inserted or deleted from the middle
Items have no state (text-only renders)
💡 Keys must be unique among siblings, not globally. Keys don't get passed as props — if you need the ID in the component, pass it explicitly as a separate prop.
What is the component lifecycle and how do hooks map to it?
💡 Hint: Mount → Update → Unmount — useEffect covers all three phases
Class components had explicit lifecycle methods. Hooks cover the same phases:
Class method
Hook equivalent
componentDidMount
useEffect(() => { ... }, [])
componentDidUpdate
useEffect(() => { ... }, [deps])
componentWillUnmount
useEffect(() => { return () => cleanup() }, [])
shouldComponentUpdate
React.memo + useMemo
getDerivedStateFromProps
Compute during render (no hook needed)
getSnapshotBeforeUpdate
useLayoutEffect return value (rare)
useEffect(() => {
// componentDidMount — runs after first render
const sub = subscribe(userId);
// componentWillUnmount — cleanup function
return () => sub.unsubscribe();
}, []); // [] = run once
useEffect(() => {
// componentDidUpdate for userId — runs when userId changes
fetchUser(userId);
}, [userId]);
💡 useEffect runs AFTER the browser paints. For DOM measurements that need to run synchronously before paint (to avoid flicker), use useLayoutEffect instead.
💡 Hint: Props = external inputs (immutable); state = internal memory (mutable via setState)
Props — passed in from outside, owned by the parent, read-only inside the component
State — owned and managed by the component itself, mutable via setState/useState, triggers re-render when changed
function Counter({ initialCount, step = 1 }) {
// ↑ props — given from parent, read-only here
const [count, setCount] = useState(initialCount);
// ↑ state — owned by this component
return (
);
}
When state changes: React re-renders the component and all its children.
When props change: the parent re-renders, passing new props, which causes the child to re-render.
💡 A common mistake: trying to sync state with props using useEffect. Instead, derive values during render if possible. If a prop controls the initial value, use it only in useState's initializer — not as a useEffect dep.
What is StrictMode and what does it do in development?
💡 Hint: Intentionally double-invokes render, effects to expose impure code and side effects — dev only, no production cost
React.StrictMode is a development tool that helps you find bugs by intentionally stressing your components.
What it does (dev only):
Double-invokes render functions — component function called twice to detect impure renders
Double-invokes effects — mounts, unmounts, and remounts every component to surface missing cleanup
Warns about deprecated APIs — componentWillMount, findDOMNode, etc.
// StrictMode reveals this bug:
function BadComponent() {
const items = []; // 💥 rendered twice, items reset each time — detected!
items.push('item'); // pure render must not have side effects
return
{items}
;
}
// StrictMode reveals missing cleanup:
useEffect(() => {
const ws = new WebSocket(url); // mount #1 — created
// StrictMode: unmount → cleanup should close ws
// mount #2 — second connection opened
// If no cleanup, you'd have two connections — StrictMode exposes this
return () => ws.close(); // ✅ proper cleanup
}, [url]);
// Usage
root.render(
);
💡 In React 18, StrictMode's double-effect firing is intentional preparation for a future feature (Offscreen). If your effect fires twice and causes a bug, you have a missing cleanup function — fix that, don't remove StrictMode.
Understanding Interruptible Rendering in React Fiber
💡 Hint: Consider how React Fiber's data structure and reconciliation algorithm enable the framework to handle rendering interruptions and prioritize tasks
Interruptible rendering is a key feature of React Fiber, allowing the framework to pause and resume rendering as needed. This is achieved through the use of a scheduler, which prioritizes tasks and handles context switching between them. The Fiber data structure is used to represent the component tree, and the reconciliation algorithm determines what changes need to be applied to the DOM. Interruptible rendering enables features like time slicing and concurrent mode, which improve the overall user experience by reducing the time spent on rendering and allowing for more efficient handling of user interactions.
In terms of implementation, the developer can utilize the React.startTransition API to wrap parts of the component tree that should be updated without interrupting the current render. This ensures a seamless user experience even in the face of complex, computationally expensive updates.
💡 Hint: Think about how React could efficiently update the UI without re-rendering the entire component tree on every state change
Reconciliation diffing is a process in React that helps to efficiently update the UI by comparing the new and old Virtual DOM representations. This process is crucial for optimizing the rendering of components and preventing unnecessary re-renders.
The key steps involved in reconciliation diffing include:
1. Creating a new Virtual DOM representation based on the updated state and props.
2. Comparing the new Virtual DOM with the previous one to identify the differences.
3. Updating the actual DOM by applying the minimum number of changes required to reflect the updated Virtual DOM.
There are two main types of reconciliation diffing in React:
1. Tree reconciliation: This involves comparing the Virtual DOM tree with the previous one and updating the actual DOM accordingly.
2. Component reconciliation: This involves comparing the props and state of individual components and updating them only when necessary.
💡 Hint: Think about how you can prevent a function from being called repeatedly when its inputs haven't changed, and how this relates to React's rendering cycle
The useMemo hook in React is used to memoize (remember) the result of a function so that it's not recalculated every time the component re-renders. This can be particularly useful for optimizing performance when dealing with expensive function calls.
import { useMemo } from 'react'; function Example() { const calculate = (numbers) => { let sum = 0; numbers.forEach((number) => { sum += number; }); return sum; }; const numbers = [1, 2, 3, 4, 5]; const total = useMemo(() => calculate(numbers), [numbers]); return (
The sum of numbers is: {total}
); }
In this example, useMemo ensures that the calculate function is only called when the numbers array changes, thus optimizing component performance by avoiding redundant calculations.
💡 Hint: Think about what happens when the state of a React component changes and how React decides what to update in the DOM
The React rendering process involves several key steps. When the state of a component changes, React creates a new virtual DOM representation of the component tree. This new virtual DOM is then compared to the previous virtual DOM to determine the minimum number of changes needed to update the actual DOM. This process is known as reconciliation.
In the above example, when the button is clicked, the state of the component changes, triggering a re-render. React will then create a new virtual DOM and compare it to the previous one to determine the changes needed to update the actual DOM. This process happens efficiently and automatically, allowing developers to focus on writing the application logic without worrying about the low-level details of DOM updates.
💡 Hint: Consider the implications of using a synchronous update method in a framework that is designed to be asynchronous
flushSync is a method in React that allows you to force the execution of pending updates in the React state.
When you call flushSync, React will synchronously update the DOM with the latest state changes. This can be useful when you need to ensure that the UI is updated immediately after a state change.
import { flushSync } from 'react-dom';
function App() {
const [count, setCount] = useState(0);
return (
💡 Hint: Think about how React can optimize the reconciliation process to prevent the main thread from being blocked for too long, and how this relates to the concept of 'fibers'
The React Fiber Architecture is a core concept in React that allows for efficient and incremental updates to the virtual DOM. It was introduced in React 16 as a replacement for the traditional stack-based reconciliation algorithm. The main idea behind fibers is to break down the reconciliation process into smaller, manageable units, called 'fibers', which can be paused and resumed as needed. This allows React to yield control back to the browser at regular intervals, preventing the main thread from being blocked for too long.
💡 Hint: Consider how React's state update handling has changed between versions 17 and 18, specifically how re-renders are triggered
React 18 introduced a new feature called Automatic Batching, which automatically batches multiple state updates into a single re-render. This feature improves the performance of React applications by reducing the number of re-renders.
import { useState } from 'react'; function Counter() { const [count, setCount] = useState(0); function handleClick() { setCount(count + 1); setCount(count + 1); } return (
Count: {count}
); }
In React 17 and earlier, the above code would cause the component to re-render twice. However, with React 18's Automatic Batching, the two state updates are batched into a single re-render.
Key differences: The key difference between React 17 and React 18 is how they handle state updates. In React 17, each state update triggers a re-render, whereas in React 18, multiple state updates are batched into a single re-render.
💡 Hint: Look into React's unstable_batchedUpdates method and how it can be used to improve performance
Batch rendering in React refers to the process of grouping multiple state updates together and applying them in a single render cycle. This approach helps to improve performance by reducing the number of unnecessary re-renders. To optimize batch rendering, you can use the ReactDOM.flushSync method to ensure that all updates are applied synchronously, or use the ReactDOM.unstable_batchedUpdates method to batch updates together.