# Comparing JavaScript UI Paradigms: From Imperative to Reactive Modern web development offers multiple approaches to building user interfaces. In this post, I'll compare four different implementations of the same simple application: a page with a header, a list of pioneering women in computing, and a "like" button that tracks clicks. Each implementation achieves the same result but uses fundamentally different programming paradigms. Understanding these differences can help you choose the right approach for your projects and better appreciate the evolution of web development. ## The Four Approaches 1. **Vanilla JS (Imperative)**: Direct DOM manipulation with procedural code 2. **Vanilla JS (Functional)**: Pure functions and immutability principles 3. **React (Browser-loaded)**: Component-based with hooks for state management 4. **Next.js**: Modern React framework with server components Let's examine each approach side by side:
<!--
Imperative Target: direct, procedural
Imperative Score: 9/10 |--------x-|
Imperative Patterns Exhibited in Code Below:
- Buzzwords:
- Direct DOM manipulation
- Procedural programming
- Rendering Strategy:
- One-time direct rendering
- Updates are imperatively targeted to DOM API
- Function Abstraction:
- Helper functions primarily for organization
- State Management:
- Mutable state, passed as direct references
- Separation of Concerns:
- Less separation between state, UI, and event handling
- Event Handling:
- Events trigger state modifications directly
Imperative Anti-Patterns in Code Below:
- Direct passing of state as args is an anti-pattern, but my preference
Orthogonal to Imperative-ness, My Preference:
- Global state
- Separating data and behavior
Call it: "Imperative approach with helper functions"
-->
<html>
<body>
<div id = 'app'></div>
<script>
// Helper functions
function createHeader(title) {
const header = document.createElement('h1');
header.textContent = title;
return header;
}
function createList(names) {
const ul = document.createElement('ul');
names.forEach(name => {
const li = document.createElement('li');
li.textContent = name;
ul.appendChild(li);
});
return ul;
}
function createButton(text, data, click_handler) {
const button = document.createElement('button');
button.textContent = text;
button.addEventListener('click', function() {
click_handler(data, button);
});
return button;
}
// Event handlers
function onClickIncLike(data, button) {
data.likeCount++;
button.textContent = `${appData.buttonText} (${data.likeCount})`;
}
// State
const appData = {
title: 'Vanilla JS (Imperative) Version',
items: ['Ada Lovelace', 'Grace Hopper', 'Margaret Hamilton'],
buttonText: 'Like',
likeCount: 0
};
// Imperative rendering
function HomePage(data) {
const app = document.getElementById('app');
app.appendChild(createHeader(data.title));
app.appendChild(createList(data.items));
app.appendChild(createButton(
`${data.buttonText} (${data.likeCount})`,
data,
onClickIncLike
));
}
HomePage(appData);
</script>
</body>
</html>
<!--
Functional Target: pure, composable, immutable
Functional Score: 7/10 |------x---|
Functional Patterns Exhibited in Code Below:
- Buzzwords:
- Pure functions (no side effects)
- Composition (nesting functions)
- Rendering Strategy:
- Explicit re-rendering, since full state is passed to render function
- Whole DOM tree is re-created on each update
- Function Abstraction:
- Helper functions represent higher-order components, not just organizational helpers
- State Management:
- State passed directly to functions via arguments
- Preserves immutability by never modifying the existing state
- Treats state as a series of values over time rather than a single value that changes
- Separation of Concerns:
- More separation between state, UI, and event handling
- Event Handling:
- Events trigger new state creation, not direct state modifications
Functional Anti-Patterns in Code Below:
- Side effects persist (anti-pattern), although can't avoid since DOM environment is inherently impure
Orthogonal to Functional-ness, My Preference:
- Global state
- Separating data and behavior
Call it: "Pragmatic functional approach adapted to the DOM environment"
-->
<html>
<body>
<div id='app'></div>
<script>
// Create a pure function to generate DOM elements
function createElement(type, props = {}, ...children) {
const element = document.createElement(type);
// Apply properties
Object.entries(props).forEach(([key, value]) => {
if (key === 'eventListeners') {
// Handle event listeners
Object.entries(value).forEach(([event, handler]) => {
element.addEventListener(event, handler);
});
} else if (key === 'style') {
// Handle style objects
Object.assign(element.style, value);
} else {
// Handle regular attributes
element[key] = value;
}
});
// Append children
children.flat().forEach(child => {
if (child instanceof Node) {
element.appendChild(child);
} else if (child !== null && child !== undefined) {
element.appendChild(document.createTextNode(child));
}
});
return element;
}
// Component functions that return DOM elements
function Header(title) {
return createElement('h1', {}, title);
}
function ListItem(text) {
return createElement('li', {}, text);
}
function List(items) {
return createElement('ul', {},
...items.map(item => ListItem(item))
);
}
function Button(text, onClick, count = null) {
const buttonText = count !== null ? `${text} (${count})` : text;
return createElement('button', {
eventListeners: { click: onClick }
}, buttonText);
}
// Event handlers
function handleLikeClick(data) {
// Create a new state object (keeping immutability)
const newState = {
...data,
likes: data.likes + 1
};
// Update the state (which triggers a re-render)
updateState(newState);
}
// Rendering function --- necessarily impure given the DOM environment
function renderApp(data, container) {
const app = createElement('div', {},
Header(data.title),
List(data.items),
Button(data.buttonText, () => handleLikeClick(data), data.likes)
);
container.innerHTML = ''; // IMPURE: side effect
container.appendChild(app); // IMPURE: side effect
}
// Simple state management
let currentState = {
title: 'Vanilla JS (Functional) Version',
items: ['Ada Lovelace', 'Grace Hopper', 'Margaret Hamilton'],
buttonText: 'Like',
likes: 0
};
function updateState(newState) {
currentState = newState;
const container = document.getElementById('app');
renderApp(currentState, container);
}
// Initialization when the DOM is loaded
document.addEventListener('DOMContentLoaded', initApp);
function initApp() {
const container = document.getElementById('app');
renderApp(currentState, container);
}
</script>
</body>
</html>
<!-- React! -->
<html>
<head>
<script src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
</head>
<body>
<div id = 'app'></div>
<script type = 'text/jsx'>
function Header({title}) {
console.log(title);
return (
<h1>{title}</h1>
);
}
function HomePage() {
const names = ['Ada Lovelace', 'Grace Hopper', 'Margaret Hamilton'];
const [likes, setLikes] = React.useState(0);
function handleClick() {
setLikes(likes + 1);
}
return (
<div>
<Header title='React Version' />
<ul>
{names.map((name, index) => (
<li key={index}>{name}</li>
))}
</ul>
<button onClick = {handleClick}>Like ({likes})</button>
</div>
);
}
const domNode = document.getElementById('app');
const root = ReactDOM.createRoot(domNode);
root.render(<HomePage />);
</script>
</body>
</html>
// app/page.jsx
// NextJS!
////////// IMPORTS //////////
import { LikeButton } from './like-button';
////////// COMPONENTS //////////
function Header({title}) {
console.log(title);
return (
<h1>{title}</h1>
);
}
function HomePage() {
const names = ['Ada Lovelace', 'Grace Hopper', 'Margaret Hamilton'];
return (
<div>
<Header title='React Version' />
<ul>
{names.map((name, index) => (
<li key={index}>{name}</li>
))}
</ul>
<LikeButton />
</div>
);
}
////////// EXPORTS //////////
export default HomePage;
// NextJS!
////////// DIRECTIVES //////////
'use client';
////////// IMPORTS //////////
import { useState } from 'react';
////////// COMPONENTS //////////
function LikeButton() {
const [likes, setLikes] = useState(0);
function handleClick() {
setLikes(likes + 1);
}
return (
<button onClick = {handleClick}>Like ({likes})</button>
);
}
////////// EXPORTS //////////
export { LikeButton };
export const metadata = {
title: 'Next.js',
description: 'Generated by Next.js',
}
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>{children}</body>
</html>
)
}