React is a library for building user interfaces based on a declarative approach: you describe what you want, and React handles how to update the DOM.
- Component-Based: UIs are built from isolated, reusable components that manage their own state.
- JSX: An HTML-like syntax extension for JavaScript that makes describing UI intuitive and powerful.
- Unidirectional Data Flow: Data flows via
props(parent to child) andstate(internal to component). Changes trigger React's update process.
- State change? → Render → Commit → Paint → Effects
- No state change? → UI unchanged
- Events (user-driven) → update state → trigger pipeline
- Effects (React-driven) → run after DOM update
Everything in React follows this cycle. Understanding this flow is the key to debugging and building effectively.
flowchart TD
Trigger[Trigger state or user event] --> R_start
subgraph RenderPhase [Render phase]
R_start[Read state and props]
R_run[Run component function]
R_jsx[Create JSX snapshot]
R_start --> R_run --> R_jsx
end
R_jsx --> C_start
subgraph CommitPhase [Commit phase]
C_start[Diff previous tree]
C_dom[Apply DOM updates]
C_start --> C_dom
end
C_dom --> Paint[Browser paint]
Paint --> E_start
subgraph EffectPhase [Effect phase]
E_start[Cleanup previous effect]
E_run[Run effect logic]
E_start --> E_run
end
E_run -->|setState| Trigger
C_dom -->|User interaction| Trigger
flowchart LR
subgraph Render[Render phase]
R1[Read props and state]
R2[Run component function]
R3[Return JSX snapshot]
R1 --> R2 --> R3
end
subgraph Commit[Commit phase]
C1[Diff with previous tree]
C2[Apply DOM mutations]
C1 --> C2
end
subgraph Effect[Effect phase]
E1[Run cleanup if needed]
E2[Run effect body]
E1 --> E2
end
R3 --> C1
C2 --> Paint[Browser paint]
Paint --> E1
E2 -->|setState| R1
- Render → pure calculation, no side effects
- Commit → DOM updates happen
- Paint → browser shows UI
- Effect → side effects run
- Effect → setState → back to Render (possible loop)
Render thinks, Commit mutates, Effect synchronizes.
- What it does: React calls your component function to get a "blueprint" of the UI. It calculates what the UI should look like based on current Props and State.
- The Output: A Virtual DOM tree (a lightweight JavaScript object describing the UI).
- Key Characteristics:
- Purity: Given the same inputs (props, state), a component will always return the same JSX.
- Idempotence: Rendering the component multiple times with the same inputs has no side effects.
- Crucial Rule: This phase must be a pure calculation.
- ❌ No Side Effects: No API calls, no DOM manipulation, no state changes, no timers.
- ✅ Calculate Derived State: Compute new values from existing props or state (e.g.,
const fullName = firstName + " " + lastName;).
- What it does: React takes the blueprint from the Render phase and makes it real. This process is called Reconciliation.
- React compares (diffs) the new Virtual DOM with the old one.
- It identifies the minimal set of changes required.
- It applies these changes in a performant batch to the actual browser DOM (Batching).
- The Output: Actual visual changes on the screen.
- Crucial Rule: MUTATION. This is the only phase where React touches the DOM. You should not interfere here.
- What it does: Runs side effects using the
useEffectanduseLayoutEffecthooks. Effects are for connecting your React component to the "outside world." - Timing: This phase runs after the Commit phase.
useEffect: Runs asynchronously after the browser has painted the screen. This is non-blocking and used for the vast majority of cases.useLayoutEffect: Runs synchronously after the DOM is updated but before the browser paints. Use this only for tasks that need to measure the DOM and then immediately trigger a re-render to prevent visual flickers.
- Crucial Rule: SYNCHRONIZATION. Use this phase to sync React state with systems that React doesn't control.
Confusing these two is the #1 cause of bugs in React. An Event happens because of user interaction; an Effect happens because of a data change (a render).
| Feature | Event Handler (User-Driven) | Effect (Data-Driven) |
|---|---|---|
| Trigger | The User (Click, Type, Scroll). | React (Component rendered with new data). |
| Timing | Runs immediately on user interaction. | Runs after the render is committed and painted. |
| Mental Model | "The user ordered a pizza." | "The kitchen restocks ingredients after cooking." |
| Example | onClick, onSubmit |
useEffect(() => { fetch(...) }, [id]) |
| Scenario | Event (Runs in an Event Handler) | Effect (Runs in useEffect) |
|---|---|---|
| Buying Item | User clicks "Buy". You send a POST request. ✅ |
❌ (This is a direct result of a user action, not synchronization.) |
| Data Fetching | N/A | Component mounts or an ID changes; fetch data to display it. ✅ |
| Search Filter | User types in a box (updates filter state). ✅ |
filter state has changed; sync it with the URL query params. ✅ |
| Toast Message | User clicks "Save". You call showToast(). ✅ |
❌ (This is an immediate response to a user action.) |
| Connecting | N/A | Component mounts; connect to a chat room or a service. ✅ |
The Core Rule:
If the code runs because the user did something, put it in an Event Handler. If the code runs because the component appeared or data changed, put it in an Effect.
The most common useEffect crash is the infinite loop. It happens because the Effect triggers the very thing it is watching.
graph LR
Render --> Effect
Effect -- "setState(...)" --> Update
Update -- "Triggers Re-render" --> Render
style Effect fill:#ff6b6b,stroke:#333,stroke-width:2px
style Update fill:#ff6b6b,stroke:#333,stroke-width:2px
The Mistake ❌
useEffect(() => {
// 1. This runs because 'count' changed.
setCount(count + 1); // 2. But this changes 'count' again, triggering a re-run.
}, [count]); // 3. The effect is watching 'count'.The Solutions ✅
If your new state depends on the previous state, use the callback form of setState. This removes the need for the state variable in the dependency array.
useEffect(() => {
const interval = setInterval(() => {
setCount((c) => c + 1); // ✅ 'c' is the guaranteed current value
}, 1000);
return () => clearInterval(interval);
}, []); // ✅ Dependency array is empty! No loop.Only run the state update if a specific condition is met.
useEffect(() => {
if (count < 5) {
setCount((c) => c + 1);
}
}, [count]); // ✅ Stops looping when count hits 5.If the state change should happen in response to a user action, it belongs in an event handler, not an effect.
// ✅ Move logic to where the user interacts.
const handleClick = () => {
setCount((c) => c + 1);
};If you are using an effect just to calculate a value from other state or props, delete the effect. Do the calculation directly in the render phase.
// ❌ Bad: Causes an unnecessary re-render.
useEffect(() => setTotal(a + b), [a, b]);
// ✅ Good: Calculates during the Render Phase (fast and safe).
const total = a + b;Effects can return a "cleanup" function. React runs the cleanup before re-running the effect or when the component unmounts. This is crucial for preventing memory leaks.
useEffect(() => {
// 1. Subscribe to a service.
const subscription = chatAPI.subscribe(id);
// 2. Return a cleanup function to unsubscribe.
return () => {
subscription.unsubscribe();
};
}, [id]); // Re-subscribes if `id` changes.This array tells React when to re-run your effect.
[](Empty Array): The effect runs once after the initial render.[dep1, dep2](Has Dependencies): The effect runs after the initial render AND any timedep1ordep2changes.- No Array: The effect runs after every single render. Avoid this.
React 19 introduces a hook to separate an effect's Reactivity (when it runs) from its Logic (what values it uses).
The Problem: An effect needs the latest value of some prop/state but shouldn't re-run just because that value changed.
The Solution: Wrap the logic in useEffectEvent to create a stable function that always sees the latest values.
// 1. Create a stable event to hold the logic.
const onVisit = useEffectEvent((url) => {
analytics.log(url, currentUser); // Reads fresh 'currentUser' without depending on it.
});
// 2. Use the stable event inside the effect.
useEffect(() => {
onVisit(url);
}, [url]); // ✅ Effect ONLY re-runs when the URL changes.Memoization is a cache for expensive operations. It prevents redundant work by "remembering" results.
useMemo(): Caches a value (the result of a heavy calculation).- Analogy: Writing the answer to a tough math problem on a whiteboard so you don't have to solve it again.
useCallback(): Caches a function definition.- Analogy: Laminating a set of instructions so you don't have to re-write them for your team every day.
React.memo(): Caches a component. If its props don't change, React skips re-rendering it and uses the last rendered result.- Analogy: If an employee is given the exact same task as yesterday, they hand over yesterday's completed report instantly.
The Context API passes data through the component tree without "prop drilling."
- Use for: Global data that changes infrequently (e.g., Theme, User Authentication, Language).
- Avoid for: High-frequency updates (e.g., form inputs, mouse position), as this will cause all consuming components to re-render, potentially hurting performance.
Error Boundaries are components that catch JavaScript errors anywhere in their child component tree, log those errors, and display a fallback UI instead of crashing the entire application.
- Implementation: A class component with
static getDerivedStateFromError()to render a fallback UI and/orcomponentDidCatch()to log error information. - Usage: Wrap parts of your UI that might fail.
<ErrorBoundary> <MyProblematicComponent /> </ErrorBoundary>
- Note: They do not catch errors in event handlers, async code, or the error boundary itself.
- Localize State: Keep state as close as possible to where it's used.
- Lazy Loading: Defer loading of non-critical components with
React.lazyand<Suspense />. - Virtualize Lists: For long lists, only render the items currently visible on screen (e.g., with
react-window). - Optimize Assets: Compress images and use modern formats.
- Profile Your App: Use the React DevTools Profiler to find performance bottlenecks.
- Render is Pure: No side effects in the main function body.
- Commit is for Mutations: React touches the DOM here; you shouldn't.
- Effects are for Syncing: Only use
useEffectto sync with external systems (APIs, DOM, Timers). - Events are for Interactions: Prefer Event Handlers over Effects for user-driven logic.
- Avoid Infinite Loops: Never unconditionally
setStateinside auseEffectthat listens to that same state. - Cleanup Your Effects: Prevent memory leaks by returning a cleanup function from
useEffect. - Memoize Wisely: Use
useMemo,useCallback, andReact.memoto prevent expensive re-renders, but don't optimize prematurely.