You're Not Ready For React - 1/4
The Uncomfortable Reality: You Think You Know Frontend Development
You’ve just survived our networking deep-dive, where you learned how computers actually talk to each other across the internet. You’ve mastered asynchronous operations, implemented resilient retry logic, and built caching that would make a CDN jealous. You probably feel pretty accomplished right now.
Here’s the thing: all of that backend wizardry is worthless if your users can’t interact with your data effectively. And right now, if you’re jumping straight into React or Vue without understanding what happens underneath, you’re building on quicksand.
Before you even think about touching a modern framework, you need to understand the fundamental problem it solves. You need to experience the raw, unfiltered pain of vanilla DOM manipulation. Not because I’m sadistic (okay, maybe a little), but because understanding the problem makes you appreciate the solution.
Today, we’re going back to basics. We’re going to build dynamic user interfaces the hard way, with nothing but vanilla JavaScript and the DOM. By the end, you’ll understand exactly why frameworks exist and why every frontend developer owes a debt of gratitude to the brilliant minds who created them.
The DOM: Your Beautiful, Terrible Friend
Let’s start with what the DOM actually is, because “it’s just a tree” is the kind of oversimplification that gets junior developers into trouble.
The Document Object Model (DOM) is the browser’s in-memory representation of your HTML document. Think of your HTML as architectural blueprints, and the DOM as the actual building constructed from those plans. Every <div>
, <p>
, <button>
, and <img>
in your HTML becomes a node in this tree structure.
This tree metaphor isn’t just academic—it directly affects how you interact with your page. You traverse it (moving from parent to child nodes), you modify its branches (changing attributes and content), and you can prune entire sections (removing elements). Each operation has a cost, and understanding that cost is crucial for building performant applications.
Here’s where it gets interesting: the DOM isn’t just a passive data structure. It’s a living, reactive system. Every change you make triggers a cascade of calculations:
- Recalculate styles: Which CSS rules apply to the modified elements?
- Reflow: Do the changes affect the layout and positioning of other elements?
- Repaint: Which pixels on the screen need to be redrawn?
Make too many changes inefficiently, and your user’s browser will struggle to keep up. This is why naive DOM manipulation can turn a smooth interface into a slideshow.
The Vanilla JavaScript Gauntlet
Let’s rebuild our user list from the networking series, but this time we’re doing it the old-school way. No shortcuts, no abstractions—just raw DOM manipulation in all its verbose glory.
Here’s our foundation:
index.html
:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />
<title>Vanilla DOM Manipulation</title>
<link rel=\"stylesheet\" href=\"style.css\" />
<script src=\"script.js\" defer></script>
</head>
<body>
<div class=\"container\">
<h1>User Management Interface</h1>
<button id=\"fetch-button\">Load Users</button>
<button id=\"refresh-button\">Refresh User List</button>
<div id=\"status\"></div>
<ul id=\"user-list\"></ul>
</div>
</body>
</html>
style.css
:
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700&display=swap");
body {
font-family: "Inter", sans-serif;
background-color: #111827;
color: #d1d5db;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
margin: 0;
padding: 2rem;
}
.container {
width: 100%;
max-width: 600px;
background-color: #1f2937;
border-radius: 0.75rem;
padding: 2rem;
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
border: 1px solid #374151;
}
h1 {
color: #f9fafb;
text-align: center;
margin-bottom: 1.5rem;
}
button {
display: block;
width: 100%;
padding: 0.75rem 1rem;
font-size: 1rem;
font-weight: 500;
color: #f9fafb;
background-color: #3b82f6;
border: none;
border-radius: 0.5rem;
cursor: pointer;
transition: background-color 0.2s;
margin-bottom: 1.5rem;
}
button:hover {
background-color: #2563eb;
}
button:disabled {
background-color: #374151;
color: #9ca3af;
cursor: not-allowed;
}
#user-list {
list-style: none;
padding: 0;
}
.user-card {
background-color: #374151;
padding: 1rem;
border-radius: 0.5rem;
margin-bottom: 1rem;
border-left: 4px solid #3b82f6;
opacity: 0;
transform: translateY(10px);
animation: fadeInUp 0.3s ease forwards;
}
@keyframes fadeInUp {
to {
opacity: 1;
transform: translateY(0);
}
}
.user-card h2 {
margin: 0 0 0.5rem 0;
color: #f9fafb;
}
.user-card p {
margin: 0;
color: #9ca3af;
}
#status {
text-align: center;
min-height: 24px;
margin-bottom: 1rem;
font-weight: 500;
padding: 0.5rem;
border-radius: 0.375rem;
transition: all 0.3s ease;
}
#status.loading {
background-color: rgba(59, 130, 246, 0.1);
border: 1px solid rgba(59, 130, 246, 0.3);
}
#status.success {
background-color: rgba(34, 197, 94, 0.1);
border: 1px solid rgba(34, 197, 94, 0.3);
}
#status.error {
background-color: rgba(239, 68, 68, 0.1);
border: 1px solid rgba(239, 68, 68, 0.3);
color: #f87171;
}
Now, here’s where the fun begins. Brace yourself for the verbose reality of vanilla DOM manipulation:
script.ts
:
interface User {
id: number;
name: string;
username: string;
email: string;
}
// Global state management (the horror begins here)
let currentUserData: User[] = [];
// DOM element references
const fetchButton = document.getElementById(
"fetch-button"
) as HTMLButtonElement;
const refreshButton = document.getElementById(
"refresh-button"
) as HTMLButtonElement;
const userList = document.getElementById("user-list") as HTMLUListElement;
const statusDiv = document.getElementById("status") as HTMLDivElement;
const API_URL = "https://jsonplaceholder.typicode.com/users";
// UI Helper Functions
const setStatus = (
message: string,
type: "loading" | "success" | "error" = "loading"
) => {
statusDiv.textContent = message;
statusDiv.className = type;
};
// The main event: rendering users with vanilla DOM manipulation
const renderUsersVanilla = (users: User[]) => {
// First, we need to clear the existing list
// Method 1: innerHTML = '' (fast but potentially memory-leaky)
// Method 2: removeChild loop (verbose but proper cleanup)
while (userList.firstChild) {
userList.removeChild(userList.firstChild);
}
// Now, the tedious process of creating each element manually
users.forEach((user, index) => {
// Create the list item container
const li = document.createElement("li");
li.className = "user-card";
// Add animation delay for staggered entrance
li.style.animationDelay = `${index * 0.1}s`;
// Create and configure the name/username heading
const h2 = document.createElement("h2");
h2.textContent = `${user.name} (@${user.username})`;
// Create and configure the email paragraph
const p = document.createElement("p");
p.textContent = user.email;
// Assemble the hierarchy
li.appendChild(h2);
li.appendChild(p);
// Insert into the DOM
userList.appendChild(li);
});
};
// Enhanced version with error boundary and loading states
const fetchUsers = async () => {
// Update UI to show loading state
fetchButton.disabled = true;
refreshButton.disabled = true;
setStatus("Loading users from API...", "loading");
try {
const response = await fetch(API_URL);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const users: User[] = await response.json();
// Store globally for later manipulation
currentUserData = users;
// Render to DOM
renderUsersVanilla(users);
setStatus(`Successfully loaded ${users.length} users`, "success");
} catch (error) {
console.error("Failed to fetch users:", error);
setStatus(
`Failed to load users: ${
error instanceof Error ? error.message : "Unknown error"
}`,
"error"
);
} finally {
// Always re-enable buttons
fetchButton.disabled = false;
refreshButton.disabled = false;
}
};
// The horror of manual state updates
const refreshUserList = () => {
if (currentUserData.length === 0) {
setStatus("No users to refresh. Load some first!", "error");
return;
}
setStatus("Refreshing user list...", "loading");
// Simulate a data transformation (reverse order)
const refreshedData = [...currentUserData].reverse();
// Here's where the pain really shows:
// Option 1: Nuclear approach - destroy and rebuild everything
renderUsersVanilla(refreshedData);
// Option 2: Intelligent diffing (what we'd need to implement manually)
// This would require:
// 1. Comparing old vs new data item by item
// 2. Finding moved, added, removed, or changed items
// 3. Performing minimal DOM operations for each change
// 4. Handling animations and transitions properly
// This is HUNDREDS of lines of complex, bug-prone code
setStatus("User list refreshed successfully", "success");
};
// Advanced: Adding inline editing capability
const makeUserEditable = (userElement: HTMLLIElement, user: User) => {
const nameElement = userElement.querySelector("h2") as HTMLHeadingElement;
const emailElement = userElement.querySelector("p") as HTMLParagraphElement;
// Double-click to edit name
nameElement.addEventListener("dblclick", () => {
const input = document.createElement("input");
input.type = "text";
input.value = user.name;
input.className = "edit-input";
// Replace h2 with input
nameElement.style.display = "none";
userElement.insertBefore(input, nameElement);
input.focus();
input.select();
const saveEdit = () => {
user.name = input.value;
nameElement.textContent = `${user.name} (@${user.username})`;
nameElement.style.display = "block";
userElement.removeChild(input);
};
input.addEventListener("blur", saveEdit);
input.addEventListener("keydown", (e) => {
if (e.key === "Enter") saveEdit();
if (e.key === "Escape") {
nameElement.style.display = "block";
userElement.removeChild(input);
}
});
});
};
// Enhanced rendering with editing capability
const renderUsersWithEditing = (users: User[]) => {
while (userList.firstChild) {
userList.removeChild(userList.firstChild);
}
users.forEach((user, index) => {
const li = document.createElement("li");
li.className = "user-card";
li.style.animationDelay = `${index * 0.1}s`;
li.dataset.userId = user.id.toString(); // For later reference
const h2 = document.createElement("h2");
h2.textContent = `${user.name} (@${user.username})`;
const p = document.createElement("p");
p.textContent = user.email;
const editHint = document.createElement("small");
editHint.textContent = "Double-click name to edit";
editHint.style.opacity = "0.6";
editHint.style.display = "block";
editHint.style.marginTop = "0.5rem";
li.appendChild(h2);
li.appendChild(p);
li.appendChild(editHint);
userList.appendChild(li);
// Add editing functionality
makeUserEditable(li, user);
});
};
// Event listeners
fetchButton.addEventListener("click", fetchUsers);
refreshButton.addEventListener("click", refreshUserList);
// Bonus: Keyboard shortcuts
document.addEventListener("keydown", (e) => {
if (e.ctrlKey && e.key === "r") {
e.preventDefault();
refreshUserList();
}
if (e.ctrlKey && e.key === "l") {
e.preventDefault();
fetchUsers();
}
});
// Clean up on page unload to prevent memory leaks
window.addEventListener("beforeunload", () => {
currentUserData = [];
while (userList.firstChild) {
userList.removeChild(userList.firstChild);
}
});
The Uncomfortable Truth About This Code
Look at what we’ve built. It works—users can load, refresh, and even edit data. But at what cost?
The Verbosity Problem: Every single element requires explicit creation, configuration, and insertion. Want to add a delete button to each user? That’s 10+ lines of code per user, repeated for every render.
The State Synchronization Problem: Notice how we have to manually keep currentUserData
in sync with the DOM. Change the data? Update the DOM. Change the DOM? Update the data. Miss one sync operation and you have bugs.
The Performance Problem: Our “refresh” function nukes the entire user list and rebuilds it from scratch. In a real application with hundreds of users, this would cause visible performance hiccups.
The Memory Leak Problem: Event listeners attached to removed DOM elements don’t automatically clean themselves up. Over time, this creates memory leaks that slow down the browser.
The Complexity Problem: Adding features like sorting, filtering, or animations requires exponentially more code. Want to animate the removal of a user? That’s dozens of lines managing animation states.
The Diffing Nightmare
Here’s the really painful part: if you wanted to update the UI efficiently, you’d need to implement what’s called “diffing”—comparing the old state with the new state and making minimal DOM changes.
This means you’d need to:
- Identify what changed: Which users were added, removed, moved, or modified?
- Plan the updates: What’s the minimal set of DOM operations needed?
- Handle edge cases: What if two users swapped positions? What if a user’s ID changed?
- Manage animations: How do you animate additions, removals, and moves without conflicts?
- Preserve state: How do you keep focus, scroll position, and form inputs intact during updates?
Professional diffing algorithms like React’s reconciler are thousands of lines of highly optimized code, developed and refined by teams of brilliant engineers over many years. Trying to implement this yourself is like trying to build a jet engine in your garage.
The Event Handling Minefield
Let’s add one more layer of complexity—proper event handling:
// Event delegation pattern for dynamic content
const setupEventDelegation = () => {
// Instead of attaching events to each user card individually,
// we attach one event listener to the parent container
userList.addEventListener("click", (e) => {
const target = e.target as HTMLElement;
const userCard = target.closest(".user-card") as HTMLLIElement;
if (!userCard) return;
const userId = userCard.dataset.userId;
if (target.matches(".delete-btn")) {
handleDeleteUser(userId!);
} else if (target.matches(".edit-btn")) {
handleEditUser(userId!);
} else if (target.matches(".favorite-btn")) {
handleFavoriteUser(userId!);
}
});
};
const handleDeleteUser = (userId: string) => {
// Find and remove from data
const index = currentUserData.findIndex((u) => u.id.toString() === userId);
if (index === -1) return;
currentUserData.splice(index, 1);
// Find and remove from DOM
const userElement = userList.querySelector(`[data-user-id="${userId}"]`);
if (userElement) {
// Add exit animation
userElement.style.animation = "fadeOut 0.3s ease forwards";
setTimeout(() => {
if (userElement.parentNode) {
userElement.parentNode.removeChild(userElement);
}
}, 300);
}
};
This is the “proper” way to handle events in vanilla JavaScript—using event delegation to avoid memory leaks and improve performance. But look how complex this is for simple operations!
What We’ve Learned (and Why It Matters)
By now, you should be feeling a mixture of accomplishment and exhaustion. You’ve successfully built a dynamic user interface with vanilla JavaScript, complete with:
- Dynamic rendering of complex data structures
- State management between data and UI
- Event handling with proper cleanup
- Error boundaries and loading states
- Performance considerations like event delegation
- Memory leak prevention
But you’ve also experienced firsthand why frontend development was considered a nightmare before modern frameworks. Every simple feature requires enormous amounts of boilerplate code, careful state synchronization, and meticulous attention to performance and memory management.
The Light at the End of the Tunnel
This is exactly the problem that React, Vue, Angular, and other modern frameworks solve. They provide:
- Declarative syntax: Describe what you want, not how to build it
- Automatic diffing: Efficient DOM updates without manual optimization
- Component abstraction: Reusable UI pieces with encapsulated logic
- State management: Predictable data flow and automatic UI synchronization
- Memory management: Automatic cleanup of event listeners and references
When you write <UserCard user={userData} />
in React, you’re not just writing less code—you’re avoiding hundreds of potential bugs and performance pitfalls that we’ve just demonstrated.
In our next article, we’ll start building our own mini-framework to solve these problems. You’ll see how the concepts behind React actually work, and why every frontend developer owes a debt of gratitude to the brilliant minds who figured this out so we don’t have to.
But for now, take a moment to appreciate what you’ve accomplished. You’ve built a real application with vanilla JavaScript, and more importantly, you now understand exactly why we don’t do it this way anymore.
The frameworks didn’t make us lazy—they made us sane.