Episode 2 — React Frontend Architecture NextJS / 2.1 — Introduction to React

2.1.b — Declarative vs Imperative UI

In one sentence: Declarative programming tells the computer WHAT you want (the end result), while imperative programming tells it HOW to get there (step-by-step instructions) — React's declarative approach is the foundation of everything it does.

Navigation: ← Why React Exists · Next → Single Page Applications


Table of Contents

  1. The Two Ways to Think About Programming
  2. Imperative Programming — The "How" Approach
  3. Declarative Programming — The "What" Approach
  4. Real-World Analogies
  5. Imperative DOM Manipulation — A Deep Example
  6. Declarative UI with React — The Same Example
  7. Side-by-Side Comparison
  8. Why Declarative Wins for UI
  9. The Declarative Spectrum — Nothing Is Purely One or the Other
  10. Declarative Patterns You Already Know
  11. How React Implements Declarative UI Under the Hood
  12. Common Mistakes — Writing Imperative Code in React
  13. When Imperative Is Still Necessary in React
  14. Declarative UI in Other Frameworks
  15. Key Takeaways

1. The Two Ways to Think About Programming

Every piece of code you write falls somewhere on a spectrum between two paradigms:

┌────────────────────────────────────────────────────────┐
│                  THE PROGRAMMING SPECTRUM               │
├────────────────────────────────────────────────────────┤
│                                                          │
│  IMPERATIVE ◄─────────────────────────────► DECLARATIVE │
│                                                          │
│  "HOW to do it"                     "WHAT I want"       │
│                                                          │
│  Step-by-step                       End result          │
│  instructions                       description         │
│                                                          │
│  Assembly    C    JavaScript    SQL    HTML    React     │
│  ◄───────────────────────────────────────────────────►  │
│  More imperative                    More declarative     │
│                                                          │
└────────────────────────────────────────────────────────┘

These aren't binary categories — they're a spectrum. The same language can be used more imperatively or more declaratively depending on how you write it.

The Core Distinction

Imperative: You specify the exact sequence of operations to achieve a result.

"Walk 3 blocks north. Turn right. Walk 2 blocks east. The restaurant is on your left."

Declarative: You specify the desired result and let something else figure out the steps.

"Take me to Mario's Italian Restaurant at 123 Main Street."

Both get you to the restaurant. But declarative is:

  • Easier to communicate — you don't need to know the route
  • Adaptable — if a road is closed, the GPS recalculates automatically
  • Less error-prone — you can't take a wrong turn if you're not giving directions

2. Imperative Programming — The "How" Approach

Imperative code reads like a recipe. You tell the computer exactly what to do, step by step.

Example: Making a Sandwich (Imperative)

1. Get two slices of bread from the bag
2. Place them on the cutting board
3. Open the peanut butter jar
4. Pick up a knife
5. Scoop peanut butter with the knife
6. Spread peanut butter on the first slice
7. Put the knife down
8. Open the jelly jar
9. Pick up the knife again
10. Scoop jelly with the knife
11. Spread jelly on the second slice
12. Place the first slice on top of the second slice, peanut butter side down
13. Cut diagonally

Example: Updating a User Profile (Imperative JavaScript)

// Imperative: step-by-step DOM manipulation
function updateUserProfile(user) {
  // Step 1: Find the name element
  const nameElement = document.getElementById('user-name');
  
  // Step 2: Update the name text
  nameElement.textContent = user.name;
  
  // Step 3: Find the avatar image
  const avatarElement = document.getElementById('user-avatar');
  
  // Step 4: Update the avatar source
  avatarElement.src = user.avatarUrl;
  
  // Step 5: Find the bio element
  const bioElement = document.getElementById('user-bio');
  
  // Step 6: Update the bio
  bioElement.textContent = user.bio;
  
  // Step 7: Find the status indicator
  const statusElement = document.getElementById('user-status');
  
  // Step 8: Update status class
  statusElement.className = user.isOnline ? 'status online' : 'status offline';
  
  // Step 9: Update status text
  statusElement.textContent = user.isOnline ? 'Online' : 'Offline';
  
  // Step 10: Find the admin badge
  const adminBadge = document.getElementById('admin-badge');
  
  // Step 11: Show or hide based on role
  adminBadge.style.display = user.role === 'admin' ? 'block' : 'none';
  
  // Step 12: Find the posts count
  const postsCount = document.getElementById('posts-count');
  
  // Step 13: Update the count
  postsCount.textContent = `${user.postsCount} posts`;
  
  // Step 14: Update the page title
  document.title = `${user.name}'s Profile`;
}

14 steps, 14 potential points of failure. Miss one? Bug. Reorder them wrong? Bug. What if user-status element doesn't exist yet? Runtime error.

Characteristics of Imperative Code

PropertyDescription
Explicit control flowYou control exactly what happens and in what order
Mutable stateYou modify existing data/DOM in place
Side effects everywhereEach step changes something external
Hard to parallelizeSteps often depend on each other's order
Easy to breakChanging one step might break subsequent steps
VerboseMany lines for simple outcomes

3. Declarative Programming — The "What" Approach

Declarative code reads like a specification. You describe what you want, and something else figures out how to make it happen.

Example: Making a Sandwich (Declarative)

I want a PB&J sandwich, cut diagonally.

The kitchen (the "runtime") knows how to make a PB&J. You don't need to specify the steps.

Example: Updating a User Profile (Declarative React)

function UserProfile({ user }) {
  return (
    <div className="user-profile">
      <img 
        id="user-avatar" 
        src={user.avatarUrl} 
        alt={`${user.name}'s avatar`} 
      />
      <h1 id="user-name">{user.name}</h1>
      <p id="user-bio">{user.bio}</p>
      
      <span className={`status ${user.isOnline ? 'online' : 'offline'}`}>
        {user.isOnline ? 'Online' : 'Offline'}
      </span>
      
      {user.role === 'admin' && <span className="admin-badge">Admin</span>}
      
      <p className="posts-count">{user.postsCount} posts</p>
    </div>
  );
}

No step-by-step instructions. No "find this element, update that property." Just: "Here's what the profile should look like for this user data."

When user changes:

  1. React calls UserProfile() with new user data
  2. It returns a new description of the UI
  3. React compares old description to new description
  4. React applies only the actual changes to the DOM

You never think about which specific DOM operations are needed.

Characteristics of Declarative Code

PropertyDescription
Result-focusedYou describe the desired output, not the steps
Immutable mindsetYou create new descriptions rather than modifying existing ones
Minimal side effectsThe "how" is hidden inside the abstraction
ComposableDeclarations combine naturally
Resilient to changeAdding a new field just means adding to the description
ConciseThe intent is clear without implementation details

4. Real-World Analogies

Analogy 1: Building a House

Imperative (you're the contractor):

1. Dig a 2-foot deep trench for the foundation
2. Build wooden forms 18 inches high
3. Mix concrete: 1 part cement, 2 parts sand, 3 parts gravel
4. Pour concrete into forms
5. Wait 7 days for curing
6. Remove forms
7. Stack concrete blocks on foundation
8. Apply mortar between each layer
...
(200 more steps)

Declarative (you're the homeowner):

I want a 3-bedroom, 2-bathroom house with:
- Open floor plan
- Kitchen island
- Hardwood floors
- Southern-facing windows

The architect and contractors handle the "how." You described the "what."

Analogy 2: Ordering Food

Imperative:

1. Go to the refrigerator
2. Take out lettuce, tomatoes, chicken, and cheese
3. Wash the lettuce under cold water for 30 seconds
4. Pat dry with paper towels
5. Dice tomatoes into 1cm cubes
6. Slice chicken into strips
7. Heat pan to medium-high...

Declarative:

"I'll have the Caesar salad with grilled chicken, please."

Analogy 3: Getting Dressed

Imperative:

1. Open the closet door
2. Look at the third shelf from top
3. Pick the blue shirt
4. Unbutton it
5. Put right arm through right sleeve
6. Put left arm through left sleeve
7. Button from bottom to top
8. Open the second drawer
9. Pick black pants
...

Declarative:

Outfit: Blue button-down shirt, black pants, brown belt, black shoes.

The Pattern

In every analogy, declarative:

  • Hides complexity — the implementation details are someone else's problem
  • Is easier to change — "Actually, make it a polo shirt" vs. rewriting 15 steps
  • Is easier to reason about — you can quickly understand the end result
  • Requires a smart runtime — someone/something needs to know HOW to fulfill your declaration

React is that smart runtime for UI development.


5. Imperative DOM Manipulation — A Deep Example

Let's build a realistic feature imperatively to feel the pain, then build it declaratively with React.

Feature: A Dynamic Shopping Cart

Requirements:

  • Display a list of items with name, price, and quantity
  • User can increase/decrease quantity
  • Show subtotal per item, total for cart
  • Show "Empty Cart" message when no items
  • Show a discount banner if total > $100
  • Disable "decrease" button when quantity is 1
  • Show item count in a badge

Imperative Version (Vanilla JS)

// State
let cartItems = [
  { id: 1, name: 'React Textbook', price: 49.99, quantity: 1 },
  { id: 2, name: 'USB-C Cable', price: 12.99, quantity: 2 },
  { id: 3, name: 'Mechanical Keyboard', price: 89.99, quantity: 1 },
];

// DOM references
const cartContainer = document.getElementById('cart');
const totalElement = document.getElementById('cart-total');
const badgeElement = document.getElementById('cart-badge');
const emptyMessage = document.getElementById('empty-message');
const discountBanner = document.getElementById('discount-banner');
const itemsList = document.getElementById('cart-items');

function render() {
  // Calculate totals
  const total = cartItems.reduce(
    (sum, item) => sum + item.price * item.quantity, 0
  );
  const itemCount = cartItems.reduce(
    (sum, item) => sum + item.quantity, 0
  );
  
  // Update total display
  totalElement.textContent = `Total: $${total.toFixed(2)}`;
  
  // Update badge
  if (itemCount > 0) {
    badgeElement.textContent = itemCount;
    badgeElement.style.display = 'inline-block';
  } else {
    badgeElement.style.display = 'none';
  }
  
  // Show/hide empty message
  if (cartItems.length === 0) {
    emptyMessage.style.display = 'block';
    itemsList.style.display = 'none';
  } else {
    emptyMessage.style.display = 'none';
    itemsList.style.display = 'block';
  }
  
  // Show/hide discount banner
  if (total > 100) {
    discountBanner.style.display = 'block';
    discountBanner.textContent = 
      `You qualify for 10% off! Save $${(total * 0.1).toFixed(2)}`;
  } else {
    discountBanner.style.display = 'none';
  }
  
  // Clear and rebuild item list
  // WARNING: This destroys all existing event listeners!
  itemsList.innerHTML = '';
  
  cartItems.forEach(item => {
    const row = document.createElement('div');
    row.className = 'cart-item';
    row.setAttribute('data-id', item.id);
    
    const nameEl = document.createElement('span');
    nameEl.className = 'item-name';
    nameEl.textContent = item.name;
    
    const priceEl = document.createElement('span');
    priceEl.className = 'item-price';
    priceEl.textContent = `$${item.price.toFixed(2)}`;
    
    const quantityControls = document.createElement('div');
    quantityControls.className = 'quantity-controls';
    
    const decreaseBtn = document.createElement('button');
    decreaseBtn.textContent = '-';
    decreaseBtn.className = 'qty-btn decrease';
    decreaseBtn.disabled = item.quantity <= 1;
    decreaseBtn.addEventListener('click', () => {
      if (item.quantity > 1) {
        item.quantity--;
        render(); // Re-render everything
      }
    });
    
    const quantityDisplay = document.createElement('span');
    quantityDisplay.className = 'quantity';
    quantityDisplay.textContent = item.quantity;
    
    const increaseBtn = document.createElement('button');
    increaseBtn.textContent = '+';
    increaseBtn.className = 'qty-btn increase';
    increaseBtn.addEventListener('click', () => {
      item.quantity++;
      render(); // Re-render everything
    });
    
    const subtotalEl = document.createElement('span');
    subtotalEl.className = 'subtotal';
    subtotalEl.textContent = `$${(item.price * item.quantity).toFixed(2)}`;
    
    const removeBtn = document.createElement('button');
    removeBtn.textContent = 'Remove';
    removeBtn.className = 'remove-btn';
    removeBtn.addEventListener('click', () => {
      cartItems = cartItems.filter(i => i.id !== item.id);
      render(); // Re-render everything
    });
    
    quantityControls.appendChild(decreaseBtn);
    quantityControls.appendChild(quantityDisplay);
    quantityControls.appendChild(increaseBtn);
    
    row.appendChild(nameEl);
    row.appendChild(priceEl);
    row.appendChild(quantityControls);
    row.appendChild(subtotalEl);
    row.appendChild(removeBtn);
    
    itemsList.appendChild(row);
  });
}

// Initial render
render();

That's ~95 lines of DOM manipulation code for a shopping cart. And it has problems:

  1. innerHTML = '' destroys everything — event listeners, focus state, scroll position
  2. Rebuilding the entire list on every change — even if only one item's quantity changed
  3. State mutationitem.quantity-- directly mutates the array objects
  4. No separation of concerns — rendering logic, event handling, and state management are tangled
  5. Adding a feature requires touching render() — growing ever more complex

6. Declarative UI with React — The Same Example

import { useState } from 'react';

function ShoppingCart() {
  const [items, setItems] = useState([
    { id: 1, name: 'React Textbook', price: 49.99, quantity: 1 },
    { id: 2, name: 'USB-C Cable', price: 12.99, quantity: 2 },
    { id: 3, name: 'Mechanical Keyboard', price: 89.99, quantity: 1 },
  ]);
  
  const total = items.reduce((sum, item) => sum + item.price * item.quantity, 0);
  const itemCount = items.reduce((sum, item) => sum + item.quantity, 0);
  
  function updateQuantity(id, delta) {
    setItems(prev =>
      prev.map(item =>
        item.id === id
          ? { ...item, quantity: Math.max(1, item.quantity + delta) }
          : item
      )
    );
  }
  
  function removeItem(id) {
    setItems(prev => prev.filter(item => item.id !== id));
  }
  
  return (
    <div className="cart">
      <h2>
        Shopping Cart
        {itemCount > 0 && <span className="badge">{itemCount}</span>}
      </h2>
      
      {total > 100 && (
        <div className="discount-banner">
          You qualify for 10% off! Save ${(total * 0.1).toFixed(2)}
        </div>
      )}
      
      {items.length === 0 ? (
        <p className="empty-message">Your cart is empty</p>
      ) : (
        <div className="cart-items">
          {items.map(item => (
            <CartItem
              key={item.id}
              item={item}
              onUpdateQuantity={updateQuantity}
              onRemove={removeItem}
            />
          ))}
        </div>
      )}
      
      <div className="cart-total">
        Total: ${total.toFixed(2)}
      </div>
    </div>
  );
}

function CartItem({ item, onUpdateQuantity, onRemove }) {
  const subtotal = item.price * item.quantity;
  
  return (
    <div className="cart-item">
      <span className="item-name">{item.name}</span>
      <span className="item-price">${item.price.toFixed(2)}</span>
      
      <div className="quantity-controls">
        <button
          className="qty-btn"
          onClick={() => onUpdateQuantity(item.id, -1)}
          disabled={item.quantity <= 1}
        >
          -
        </button>
        <span className="quantity">{item.quantity}</span>
        <button
          className="qty-btn"
          onClick={() => onUpdateQuantity(item.id, 1)}
        >
          +
        </button>
      </div>
      
      <span className="subtotal">${subtotal.toFixed(2)}</span>
      <button className="remove-btn" onClick={() => onRemove(item.id)}>
        Remove
      </button>
    </div>
  );
}

What's Different

Read the React version top-to-bottom. Can you understand the UI without running the code? The cart shows a badge when there are items, a discount banner when total > $100, either an empty message or the item list, and a total at the bottom. Each item shows its details with quantity controls.

You described WHAT the UI should look like. React handles HOW to update the DOM.


7. Side-by-Side Comparison

AspectImperative (Vanilla JS)Declarative (React)
Lines of code~95~80
DOM queries6 (getElementById, etc.)0
createElement calls11 per item0
addEventListener calls3 per item + cleanup risk0 (inline handlers, auto-cleanup)
innerHTML usageYes (XSS risk, destroys state)No
State managementMutable array + direct mutationImmutable updates via setItems
UI descriptionImperative DOM constructionDeclarative JSX
Adding a featureModify render() + add DOM elementsAdd state + JSX
ReusabilityNoneCartItem component is reusable
TestingRequires DOM environmentComponent is a pure function
Focus preservationLost on re-renderReact preserves it
AccessibilityMust manually manageReact handles element identity

The Critical Difference: What Happens When State Changes

Imperative:

  1. Developer identifies which DOM nodes need to change
  2. Developer writes code to change each one
  3. Developer must handle edge cases (element doesn't exist, transition states)
  4. If developer misses a DOM node, it shows stale data

Declarative:

  1. Developer calls setState with new data
  2. React re-runs the component function
  3. React diffs the old and new output
  4. React applies only the necessary DOM changes
  5. Every piece of UI automatically reflects the new state — impossible to miss one

8. Why Declarative Wins for UI

Reason 1: Predictability

// Given this state:
const user = { name: 'Alice', isAdmin: true, postsCount: 42 };

// This function ALWAYS produces the same output:
function UserCard({ user }) {
  return (
    <div>
      <h2>{user.name}</h2>
      {user.isAdmin && <span className="badge">Admin</span>}
      <p>{user.postsCount} posts</p>
    </div>
  );
}

// I can predict the output without running the code:
// <div>
//   <h2>Alice</h2>
//   <span class="badge">Admin</span>
//   <p>42 posts</p>
// </div>

With imperative code, the output depends on the history of operations — what happened before, in what order, what state the DOM was in. With declarative code, the output depends only on the current input.

Reason 2: Maintainability

Adding a new requirement to imperative code means:

  1. Find the render() function
  2. Add new DOM creation code
  3. Add event listeners
  4. Make sure it's in the right position
  5. Make sure it doesn't break existing elements
  6. Update the cleanup logic

Adding a new requirement to React:

  1. Add state if needed
  2. Add JSX where you want it to appear
  3. Done
// Before: No "last updated" display
function UserProfile({ user }) {
  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.bio}</p>
    </div>
  );
}

// After: Add "last updated" — just add one line of JSX
function UserProfile({ user }) {
  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.bio}</p>
      <time>{new Date(user.updatedAt).toLocaleDateString()}</time>  {/* NEW */}
    </div>
  );
}

Reason 3: Composability

Declarative components compose naturally:

// Small components
function SearchBar({ value, onChange }) {
  return <input value={value} onChange={e => onChange(e.target.value)} />;
}

function FilterDropdown({ options, value, onChange }) {
  return (
    <select value={value} onChange={e => onChange(e.target.value)}>
      {options.map(opt => <option key={opt} value={opt}>{opt}</option>)}
    </select>
  );
}

function ProductGrid({ products }) {
  return (
    <div className="grid">
      {products.map(p => <ProductCard key={p.id} product={p} />)}
    </div>
  );
}

// Composed into a page — reads like a description of the UI
function CatalogPage() {
  const [search, setSearch] = useState('');
  const [category, setCategory] = useState('all');
  const products = useFilteredProducts(search, category);
  
  return (
    <div>
      <SearchBar value={search} onChange={setSearch} />
      <FilterDropdown 
        options={['all', 'electronics', 'books']} 
        value={category} 
        onChange={setCategory} 
      />
      <ProductGrid products={products} />
    </div>
  );
}

Reason 4: Debugging

Imperative debugging:

Bug: The cart total shows wrong number.
Question: At which step in the 95-line render function did it go wrong?
Did we compute the total correctly?
Did we update the right DOM element?
Did another function modify the total element afterward?
Is there a race condition with an async update?

Declarative debugging:

Bug: The cart total shows wrong number.
Question: What's the current state?
  → items = [{id: 1, price: 49.99, quantity: 2}, ...]
  → total = items.reduce((sum, i) => sum + i.price * i.quantity, 0)
  → total = 99.98
  → That's correct. The bug must be in the state update, not the rendering.

In React, bugs are almost always in the state logic, not the rendering logic. This dramatically narrows where to look.

Reason 5: Performance (Counter-intuitive)

"Wait, re-rendering everything every time state changes sounds SLOW."

It would be — if React actually touched the DOM every time. But it doesn't:

┌────────────────────────────────────────────────────┐
│         HOW REACT AVOIDS WASTEFUL DOM UPDATES       │
├────────────────────────────────────────────────────┤
│                                                      │
│  State changes → Component re-renders                │
│       ↓                                              │
│  New Virtual DOM tree created (cheap — JS objects)   │
│       ↓                                              │
│  Diff against previous Virtual DOM tree              │
│       ↓                                              │
│  Calculate minimal set of changes                    │
│       ↓                                              │
│  Apply ONLY those changes to real DOM                │
│                                                      │
│  Example: quantity changed from 2 to 3               │
│  ─────────────────────────────────────               │
│  Virtual DOM diff finds:                             │
│  - Only the quantity text node changed               │
│  - Only the subtotal text node changed               │
│  - Badge count changed                              │
│  - Total changed                                    │
│  Actual DOM operations: 4 text updates               │
│  NOT: rebuild entire cart DOM                        │
│                                                      │
└────────────────────────────────────────────────────┘

The declarative approach (re-describe everything, let the system figure out changes) actually enables better optimization than manual DOM updates, because React can batch and minimize DOM operations in ways manual code typically doesn't.


9. The Declarative Spectrum — Nothing Is Purely One or the Other

It's important to understand that "declarative" and "imperative" aren't absolute categories. They're relative:

Most Imperative                              Most Declarative
     │                                              │
     ▼                                              ▼
Assembly → C → JavaScript → jQuery → React → SQL → HTML

HTML Is Declarative

<h1>Hello World</h1>
<ul>
  <li>Item 1</li>
  <li>Item 2</li>
</ul>

You don't tell the browser "allocate 40 pixels of height, render 'Hello World' in Times New Roman at 32px, calculate the line box..." You declare what you want, and the browser figures out the rendering.

CSS Is Declarative

.card {
  display: flex;
  gap: 16px;
  border-radius: 8px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

You don't say "calculate each child's width, position them horizontally, add 16px between each..." You declare the layout and the browser handles it.

SQL Is Declarative

SELECT name, email 
FROM users 
WHERE age > 18 
ORDER BY name;

You don't say "scan the B-tree index, compare each age field, allocate a sort buffer, apply quicksort..." You declare what data you want and the database optimizer figures out the most efficient execution plan.

React Adds Declarative UI to JavaScript

JavaScript itself is imperative. React layers declarative UI on top:

// Imperative JavaScript (under the hood)
const div = document.createElement('div');
const h1 = document.createElement('h1');
h1.textContent = 'Hello';
div.appendChild(h1);
document.body.appendChild(div);

// Declarative React (what you write)
function App() {
  return (
    <div>
      <h1>Hello</h1>
    </div>
  );
}

React translates your declarative JSX into imperative DOM operations. You write the "what," React handles the "how."


10. Declarative Patterns You Already Know

If you know HTML, CSS, or SQL, you already think declaratively. React applies the same pattern to dynamic UI.

HTML (Static Structure)

<!-- You declare structure, browser renders it -->
<nav>
  <a href="/">Home</a>
  <a href="/about">About</a>
</nav>

CSS (Static Styling)

/* You declare appearance, browser paints it */
.nav { display: flex; gap: 20px; }
.nav a { color: blue; text-decoration: none; }
.nav a:hover { text-decoration: underline; }

React (Dynamic Structure + Styling)

// You declare dynamic UI, React updates it
function Nav({ links, currentPath }) {
  return (
    <nav className="nav">
      {links.map(link => (
        <a 
          key={link.path}
          href={link.path} 
          className={link.path === currentPath ? 'active' : ''}
        >
          {link.label}
        </a>
      ))}
    </nav>
  );
}

React extends the declarative model from static documents to dynamic, interactive applications.

Array Methods Are Declarative

You already use declarative patterns in JavaScript:

// Imperative filtering
const adults = [];
for (let i = 0; i < users.length; i++) {
  if (users[i].age >= 18) {
    adults.push(users[i]);
  }
}

// Declarative filtering
const adults = users.filter(user => user.age >= 18);

// Imperative transformation
const names = [];
for (let i = 0; i < users.length; i++) {
  names.push(users[i].name.toUpperCase());
}

// Declarative transformation
const names = users.map(user => user.name.toUpperCase());

// Imperative reduction
let total = 0;
for (let i = 0; i < items.length; i++) {
  total += items[i].price;
}

// Declarative reduction
const total = items.reduce((sum, item) => sum + item.price, 0);

React's JSX uses these same declarative array methods to render lists:

function UserList({ users }) {
  return (
    <ul>
      {users
        .filter(user => user.isActive)
        .sort((a, b) => a.name.localeCompare(b.name))
        .map(user => (
          <li key={user.id}>{user.name}</li>
        ))
      }
    </ul>
  );
}

11. How React Implements Declarative UI Under the Hood

React's declarative model is powered by three mechanisms:

Mechanism 1: Virtual DOM

When you write JSX, React doesn't create real DOM elements immediately. It creates lightweight JavaScript objects — the "Virtual DOM":

// Your JSX:
<div className="card">
  <h2>Title</h2>
  <p>Description</p>
</div>

// What React creates internally (simplified):
{
  type: 'div',
  props: {
    className: 'card',
    children: [
      { type: 'h2', props: { children: 'Title' } },
      { type: 'p', props: { children: 'Description' } }
    ]
  }
}

These are plain objects. Creating and comparing them is extremely fast — orders of magnitude faster than touching the real DOM.

Mechanism 2: Reconciliation (Diffing)

When state changes, React:

  1. Calls your component function with new state
  2. Gets a new Virtual DOM tree
  3. Diffs the new tree against the previous tree
  4. Identifies exactly what changed
Previous:                       New:
┌─────────────┐                ┌─────────────┐
│ div.card     │                │ div.card     │  ← same
│  ├─ h2       │                │  ├─ h2       │  ← same
│  │  "Title"  │                │  │  "Title"  │  ← same
│  └─ p        │                │  └─ p        │  ← same
│     "Hello"  │                │     "World"  │  ← CHANGED!
└─────────────┘                └─────────────┘

Diff result: Change text content of <p> from "Hello" to "World"
DOM operations needed: 1 (textContent update)

Mechanism 3: Batched DOM Updates

React collects all necessary DOM changes and applies them in a single batch:

State update 1: name changed
State update 2: email changed  
State update 3: avatar changed

Without batching: 3 separate DOM updates → 3 reflows
With React's batching: 1 combined DOM update → 1 reflow

This is why the "re-render everything" approach is actually efficient — React batches and minimizes the actual DOM operations.

┌────────────────────────────────────────────────────┐
│          THE FULL REACT RENDER CYCLE                │
├────────────────────────────────────────────────────┤
│                                                      │
│  1. STATE CHANGES                                    │
│     setCount(5)                                      │
│          ↓                                           │
│  2. REACT RE-RENDERS COMPONENT                       │
│     Calls Counter() with new state                   │
│     Returns new Virtual DOM tree (JS objects)        │
│          ↓                                           │
│  3. DIFFING                                          │
│     Compares new tree with previous tree             │
│     Finds: only the count text changed               │
│          ↓                                           │
│  4. COMMIT (DOM UPDATE)                              │
│     Updates only the text node: "4" → "5"            │
│     Single DOM operation, single reflow              │
│          ↓                                           │
│  5. BROWSER PAINTS                                   │
│     User sees the updated count                      │
│                                                      │
│  Steps 1-3: Pure JavaScript (fast)                   │
│  Step 4: Minimal DOM touch (optimized)               │
│  Step 5: Browser native (unavoidable)                │
│                                                      │
└────────────────────────────────────────────────────┘

12. Common Mistakes — Writing Imperative Code in React

New React developers often accidentally write imperative code:

Mistake 1: Directly Manipulating the DOM

// ❌ WRONG — imperative DOM manipulation in React
function BadComponent() {
  function handleClick() {
    document.getElementById('output').textContent = 'Clicked!';
    document.getElementById('output').style.color = 'green';
  }
  
  return (
    <div>
      <button onClick={handleClick}>Click me</button>
      <p id="output">Not clicked</p>
    </div>
  );
}

// ✅ RIGHT — declarative state-driven UI
function GoodComponent() {
  const [clicked, setClicked] = useState(false);
  
  return (
    <div>
      <button onClick={() => setClicked(true)}>Click me</button>
      <p style={{ color: clicked ? 'green' : 'inherit' }}>
        {clicked ? 'Clicked!' : 'Not clicked'}
      </p>
    </div>
  );
}

Why the first is wrong: You're bypassing React's rendering system. React doesn't know the DOM changed, so it might overwrite your changes on the next render. You also lose all the benefits of React's diffing.

Mistake 2: Imperatively Showing/Hiding Elements

// ❌ WRONG — imperatively toggling visibility
function BadToggle() {
  function toggle() {
    const panel = document.getElementById('panel');
    panel.style.display = panel.style.display === 'none' ? 'block' : 'none';
  }
  
  return (
    <div>
      <button onClick={toggle}>Toggle</button>
      <div id="panel">Panel content</div>
    </div>
  );
}

// ✅ RIGHT — declaratively rendering based on state
function GoodToggle() {
  const [isOpen, setIsOpen] = useState(true);
  
  return (
    <div>
      <button onClick={() => setIsOpen(!isOpen)}>Toggle</button>
      {isOpen && <div className="panel">Panel content</div>}
    </div>
  );
}

Mistake 3: Building DOM Strings

// ❌ WRONG — building HTML strings (XSS risk!)
function BadList({ items }) {
  const listRef = useRef();
  
  useEffect(() => {
    listRef.current.innerHTML = items
      .map(item => `<li>${item.name}</li>`)
      .join('');
  }, [items]);
  
  return <ul ref={listRef}></ul>;
}

// ✅ RIGHT — declarative list rendering
function GoodList({ items }) {
  return (
    <ul>
      {items.map(item => (
        <li key={item.id}>{item.name}</li>
      ))}
    </ul>
  );
}

Mistake 4: Imperative Element Creation

// ❌ WRONG — creating DOM elements imperatively
function BadDynamic() {
  const containerRef = useRef();
  
  function addItem() {
    const li = document.createElement('li');
    li.textContent = `Item ${Date.now()}`;
    containerRef.current.appendChild(li);
  }
  
  return (
    <div>
      <button onClick={addItem}>Add Item</button>
      <ul ref={containerRef}></ul>
    </div>
  );
}

// ✅ RIGHT — state-driven list
function GoodDynamic() {
  const [items, setItems] = useState([]);
  
  function addItem() {
    setItems(prev => [...prev, { id: Date.now(), text: `Item ${Date.now()}` }]);
  }
  
  return (
    <div>
      <button onClick={addItem}>Add Item</button>
      <ul>
        {items.map(item => (
          <li key={item.id}>{item.text}</li>
        ))}
      </ul>
    </div>
  );
}

The Rule of Thumb

In React, the ONLY way to update UI is to update state. State changes → component re-renders → new JSX → React updates DOM. Never touch the DOM directly.


13. When Imperative Is Still Necessary in React

Despite React's declarative model, there are legitimate cases where you need imperative code:

Case 1: Focus Management

function SearchBar() {
  const inputRef = useRef(null);
  
  useEffect(() => {
    // Imperative: tell the input to focus
    inputRef.current.focus();
  }, []);
  
  return <input ref={inputRef} placeholder="Search..." />;
}

There's no declarative way to say "this input should be focused." You must imperatively call .focus().

Case 2: Scroll Position

function ChatMessages({ messages }) {
  const bottomRef = useRef(null);
  
  useEffect(() => {
    // Imperative: scroll to the latest message
    bottomRef.current.scrollIntoView({ behavior: 'smooth' });
  }, [messages]);
  
  return (
    <div className="chat">
      {messages.map(msg => (
        <div key={msg.id} className="message">{msg.text}</div>
      ))}
      <div ref={bottomRef} />
    </div>
  );
}

Case 3: Measuring DOM Elements

function Tooltip({ children, text }) {
  const [position, setPosition] = useState({ top: 0, left: 0 });
  const triggerRef = useRef(null);
  
  function showTooltip() {
    // Imperative: read the DOM element's position
    const rect = triggerRef.current.getBoundingClientRect();
    setPosition({
      top: rect.bottom + 8,
      left: rect.left + rect.width / 2,
    });
  }
  
  return (
    <span ref={triggerRef} onMouseEnter={showTooltip}>
      {children}
    </span>
  );
}

Case 4: Third-Party Library Integration

function Map({ center, zoom }) {
  const mapRef = useRef(null);
  const mapInstanceRef = useRef(null);
  
  useEffect(() => {
    // Imperative: initialize the map library
    mapInstanceRef.current = new google.maps.Map(mapRef.current, {
      center,
      zoom,
    });
    
    return () => {
      // Imperative: cleanup
      mapInstanceRef.current.destroy();
    };
  }, []);
  
  useEffect(() => {
    // Imperative: update the map when props change
    mapInstanceRef.current?.setCenter(center);
    mapInstanceRef.current?.setZoom(zoom);
  }, [center, zoom]);
  
  return <div ref={mapRef} style={{ height: 400 }} />;
}

Case 5: Animations

function AnimatedBox() {
  const boxRef = useRef(null);
  
  function animate() {
    // Imperative: use the Web Animations API
    boxRef.current.animate(
      [
        { transform: 'translateX(0px)', opacity: 1 },
        { transform: 'translateX(200px)', opacity: 0.5 },
        { transform: 'translateX(0px)', opacity: 1 },
      ],
      { duration: 1000, easing: 'ease-in-out' }
    );
  }
  
  return (
    <div>
      <div ref={boxRef} className="box" />
      <button onClick={animate}>Animate</button>
    </div>
  );
}

The Pattern: useRef for Imperative Escape Hatches

Notice that all imperative React code uses useRef. The ref gives you a direct reference to a DOM element, bypassing React's declarative system. Use it sparingly — it's an escape hatch, not the normal way of working.


14. Declarative UI in Other Frameworks

React isn't the only declarative UI framework. Understanding how others approach it helps solidify the concept:

Vue (Template-based Declarative)

<template>
  <div class="cart">
    <div v-if="items.length === 0">Cart is empty</div>
    <div v-else>
      <div v-for="item in items" :key="item.id" class="cart-item">
        <span>{{ item.name }}</span>
        <span>{{ item.quantity }}</span>
        <span>${{ (item.price * item.quantity).toFixed(2) }}</span>
      </div>
      <div>Total: ${{ total.toFixed(2) }}</div>
    </div>
  </div>
</template>

<script setup>
import { computed } from 'vue';

const items = ref([...]);
const total = computed(() => 
  items.value.reduce((sum, i) => sum + i.price * i.quantity, 0)
);
</script>

SwiftUI (Apple's Declarative UI)

struct CartView: View {
    @State var items: [CartItem]
    
    var total: Double {
        items.reduce(0) { $0 + $1.price * Double($1.quantity) }
    }
    
    var body: some View {
        VStack {
            if items.isEmpty {
                Text("Cart is empty")
            } else {
                ForEach(items) { item in
                    HStack {
                        Text(item.name)
                        Text("\(item.quantity)")
                        Text("$\(item.price * Double(item.quantity), specifier: "%.2f")")
                    }
                }
                Text("Total: $\(total, specifier: "%.2f")")
            }
        }
    }
}

Flutter (Widget-based Declarative)

class CartWidget extends StatelessWidget {
  final List<CartItem> items;
  
  double get total =>
      items.fold(0, (sum, item) => sum + item.price * item.quantity);

  @override
  Widget build(BuildContext context) {
    if (items.isEmpty) {
      return Text('Cart is empty');
    }
    return Column(
      children: [
        ...items.map((item) => Row(
          children: [
            Text(item.name),
            Text('${item.quantity}'),
            Text('\$${(item.price * item.quantity).toStringAsFixed(2)}'),
          ],
        )),
        Text('Total: \$${total.toStringAsFixed(2)}'),
      ],
    );
  }
}

The Common Thread

Every modern UI framework has adopted the declarative paradigm:

  • Describe what the UI should look like for given data
  • Let the framework handle DOM/rendering updates
  • State changes trigger automatic re-renders

React pioneered this approach for the web, and it's now the standard across all platforms.


15. Key Takeaways

  1. Imperative = HOW, Declarative = WHAT. Imperative code gives step-by-step instructions. Declarative code describes the desired result.

  2. React is declarative. You describe what the UI should look like for given state. React handles the DOM manipulation.

  3. The key insight: UI = f(state). Your component is a function that transforms data into a UI description. Same data → same UI, always.

  4. Declarative wins for UI because it's more predictable, maintainable, composable, and debuggable than imperative DOM manipulation.

  5. You already think declaratively when you write HTML, CSS, SQL, or use array methods like .map() and .filter().

  6. React's Virtual DOM makes declarative efficient — re-describe everything, let React diff and apply minimal DOM changes.

  7. In React, update state to update UI. Never touch the DOM directly. The only way to change what the user sees is to change the data.

  8. Imperative escape hatches exist via useRef — for focus, scroll, measurements, animations, and third-party library integration.

  9. Common React mistakes involve writing imperative code: directly manipulating DOM, building HTML strings, imperatively showing/hiding elements. Always think in terms of state.

  10. The mental shift from imperative to declarative is the single most important thing you'll learn in React. Once you internalize it, everything else falls into place.


Explain-It Challenge

  1. Explain to someone who only knows vanilla JavaScript: Why is it better to describe "what the UI should look like" instead of writing step-by-step DOM updates? Use the shopping cart example.

  2. Create your own analogy (not restaurant, GPS, or spreadsheet) that explains declarative vs imperative thinking. Bonus: find an analogy where imperative is actually better.

  3. Identify the imperative vs declarative parts in this React component — not everything in React code is declarative:

function SearchPage() {
  const [query, setQuery] = useState('');
  const inputRef = useRef(null);
  
  useEffect(() => {
    inputRef.current.focus(); // <-- Which paradigm?
  }, []);
  
  const results = products.filter(p => // <-- Which paradigm?
    p.name.includes(query)
  );
  
  return (
    <div>  {/* <-- Which paradigm? */}
      <input ref={inputRef} value={query} onChange={e => setQuery(e.target.value)} />
      {results.map(r => <div key={r.id}>{r.name}</div>)}
    </div>
  );
}

Navigation: ← Why React Exists · Next → Single Page Applications