Why React? The Evolution of Declarative UI

Why React? The Evolution of Declarative UI
Photo by Rahul Mishra / Unsplash

If you've been building websites or mobile apps for a while, you've probably felt the pain of managing complex user interfaces. Back in the early days of the web, sites were straightforward, think newspaper-style pages that simply displayed information. Then came dynamic experiences like Amazon's shopping carts, YouTube's video players, and social feeds on Twitter (now X) and Facebook. As these use cases grew more intricate, traditional ways of building UIs started to crack under the pressure.

That's where the story of React comes in. But to truly appreciate why it took over frontend development, we need to rewind and understand the shift from imperative to declarative UI. I'll walk you through the history, the problems it solved, how React works under the hood, and even how its ideas spread beyond the web. All of this is based on verified facts from React's official history and ecosystem docs.

Imperative UI: The Old-School Way

Websites are built with HTML for structure, CSS for styling, and JavaScript for interactivity. In the imperative approach, you manually tell the browser exactly how to manipulate the DOM (Document Object Model). You query elements by ID or class, create new nodes, and update properties step by step.

Here's a classic example: a simple to-do list where you add items via an input field.

<html>
<body>
  <input type="text" id="itemInput" placeholder="Enter item name">
  <button id="addBtn">Add Item</button>
  <ul id="myList"></ul>
</body>
</html>
// Query the elements by their ID
const input = document.getElementById('itemInput');
const addBtn = document.getElementById('addBtn');
const list = document.getElementById('myList');

function addItem() {
  const text = input.value.trim();
  if (!text) return;

  // Create new list item
  const li = document.createElement('li');
  li.textContent = text;

  // Add it to the unordered list
  list.appendChild(li);

  // Clear input
  input.value = '';
  input.focus();
}

// Event listener
addBtn.addEventListener('click', addItem);

As you can see, you're directly reaching into the DOM, creating elements, and appending them. The JavaScript "state" (like an array of items) lives separately from the UI. If you want to keep them in sync say, after deleting an item or loading data from an API, you end up writing a ton of manual glue code: getters, setters, event listeners, and DOM updates.

This works fine for tiny pages, but scale it up to a real app with dozens of interactive components, and things get messy fast. Inconsistent states, bugs from missed updates, and boilerplate everywhere. Developers back then often joked that their codebases felt like spaghetti.

Enter jQuery in 2006. Created by John Resig, it was a game-changer for simplifying DOM queries, animations, and cross-browser issues. It reduced boilerplate, but it was still fundamentally imperative. You were still manually poking at the DOM. It dominated the web for years (and still powers some legacy sites), but it didn't fix the core disconnect between data and UI.

The Rise of Declarative UI

The fundamental flaw? Imperative code forces you to describe how to update the UI every single time something changes. Declarative UI flips this: you describe what the UI should look like based on the current state, and the framework handles the how.

In web development, this idea wasn't brand new. AngularJS (from Google) launched in 2010 with two-way data binding, making UIs stateful and declarative from the start. But it was React, introduced by Facebook (now Meta) engineer Jordan Walke, that truly popularized the approach and made it efficient at scale.

React started as an internal Facebook tool in 2011 (originally called FaxJS) to handle their massive, real-time UIs like the News Feed. It was open-sourced in May 2013 at JSConf US. What made it special? The Virtual DOM, a lightweight in-memory representation of the real DOM. When your state changes, React diffs the virtual trees and applies only the minimal updates to the browser. No more manual DOM surgery.

The React Way: State-Driven UIs

Let's rebuild that same list in React

import { useState } from 'react';

function App() {
  const [items, setItems] = useState([]);
  const [input, setInput] = useState('');

  const addItem = () => {
    if (!input.trim()) return;
    setItems([...items, input.trim()]);
    setInput('');
  };

  return (
    <div>
      <h1>Declarative UI with React</h1>
      
      <input
        value={input}
        onChange={(e) => setInput(e.target.value)}
        placeholder="Enter item name"
      />
      <button onClick={addItem}>Add Item</button>

      <ul>
        {items.map((item, index) => (
          <li key={index}>{item}</li>  // Note: Use a stable ID in real apps!
        ))}
      </ul>
    </div>
  );
}

export default App;

No more document.getElementById or appendChild. You just maintain state with useState, and React re-renders the UI automatically whenever that state updates. The list stays perfectly in sync, no manual DOM work required.

Diving Deeper: Hooks and Side Effects

At the heart of React is state. But not every piece of data should trigger a re-render. That's why useState exists, it's a deliberate signal to React: "This value affects the UI."

Early React had class components with lifecycle methods, but hooks (introduced in 2018) made everything cleaner in functional components.

One common pitfall? Side effects like timers, API calls, or subscriptions. You can't just drop them in the component body, they'd run on every render.

Take this buggy timer:

import { useState } from "react";

export default function Timer() {
  const [seconds, setSeconds] = useState(0);

  // This runs on EVERY render — multiple timers!
  setInterval(() => {
    setSeconds((prev) => prev + 1);
  }, 1000);

  return <p>Seconds elapsed: {seconds}</p>;
}

React can't guarantee how many times your component body runs. The fix? useEffect for side effects.

import { useState, useEffect } from "react";

export default function Timer() {
  const [seconds, setSeconds] = useState(0);

  useEffect(() => {
    const id = setInterval(() => {
      setSeconds((prev) => prev + 1);
    }, 1000);

    return () => clearInterval(id);  // Cleanup on unmount
  }, []);  // Empty dependency array = run once

  return <p>Seconds elapsed: {seconds}</p>;
}

Perfect. Now it's reliable.

For performance, React gives us two more heroes:

  • useMemo caches expensive computations:
const expensiveValue = useMemo(() => {
  return heavyComputation(items);
}, [items]);  // Only re-compute when items change
  • useCallback memoizes functions to prevent unnecessary re-creations (great for passing to child components):
const handleClick = useCallback(() => {
  console.log('Clicked');
}, []);  // Empty deps = stable reference

How React Shaped the Entire Ecosystem

React didn't just win the web, it inspired a whole wave of declarative tools. Frameworks like Svelte (which compiles away the framework at build time for blazing speed) and others like Solid.js, Vue.js or Preact adopted similar state-driven, component-based thinking.

The influence went native too. On Android, the old XML-based layouts were imperative, you'd find views by ID in Java/Kotlin and update them manually. Then Google introduced Jetpack Compose in 2019: a fully declarative UI toolkit that's heavily inspired by React (and tools like Flutter and SwiftUI).

Check out this Compose equivalent of our list example:

@Composable
fun AddToListScreen() {
    // State for the list (mutable and observable)
    val items = remember { mutableStateListOf<String>() }
    // State for the text input
    var inputText by remember { mutableStateOf("") }

    Column {
        OutlinedTextField(
            value = inputText,
            onValueChange = { inputText = it },
            label = { Text("Enter item name") }
        )
        
        Button(
            onClick = {
                if (inputText.isNotBlank()) {
                    items.add(inputText.trim())  // State update triggers re-compose
                }
            }
        ) {
            Text("Add Item")
        }

        Spacer(modifier = Modifier.height(24.dp))

        // The list UI (automatically updates when items change)
        LazyColumn {
            items(items) { item ->
                Text(
                    text = item,
                    modifier = Modifier.padding(16.dp)
                )
            }
        }
    }
}

See the parallels? remember for memoization, mutableStateOf for reactive state, and LaunchedEffect/SideEffect for one-time side effects—just like React's hooks.

Why React Still Matters

React solved the real pain of building UIs at scale: keeping data and interface in perfect harmony without boilerplate nightmares. It's now used by millions of developers, powers everything from Netflix to Instagram, and continues evolving.

Next time you're writing a React component, take a second to appreciate the journey from imperative DOM hacks to this elegant, state-first world. It's not just a library; it's the reason modern frontend feels so much more enjoyable.

What do you think? still using React, or have you switched to something new? Drop your thoughts below!