We need to have a heart-to-heart about something happening in our community. Developers are using the latest AI models, shipping features in record time, but they feel like they’re losing their “edge.”
They feel, well, fake 😢
They feel like they’re just professional copy-pasters who spend 80% of their day arguing with a chatbot about why a z-index isn’t working.
This is the “almost-right” burnout. It happens because we’ve outsourced the “thinking” part of coding and kept only the “frustration” part (debugging).
To fix this, we need to intentionally step away from the AI magic and get our hands dirty with the fundamentals.
Today, we’re going to build a Manual State-Sync Dashboard. No React, no Bootstrap or Tailwind, no AI.
Just you, an index.html file, me in the background guiding you on this page, and your brain 🧠
See the Pen Manual State-Sync Dashboard by Klea Merkuri (@thehelpfultipper) on CodePen.
The Conceptual “Why”: Why Manual Matters
In 2026, it’s easy to forget that every framework—React, Vue, Svelte—is just a wrapper around the browser’s native APIs like the DOM API and Event Listeners.
When you rely 100% on AI to write these wrappers for you, you lose the mental model of how the browser actually works.
The “almost-right” code usually fails because of a misunderstanding of:
- The Event Loop: When exactly does the code run?
- The DOM Tree: Where exactly does the element live in the hierarchy?
- Reference vs. Value: Is that object a copy or the original?
By building the “boring way,” you rebuild these mental pathways.
You pay the “tax” upfront by thinking through the logic, so you don’t have to pay it later in a frantic 4:00 PM debugging session.
Related: How To Master React State & Event Handlers (Part 4)
Step 1: The Project Setup (The Zen of the Empty Folder)
Open your terminal. Create a new folder. Do not run npx create-react-app. Do not open a chat window.
mkdir back-to-basics-vibe
cd back-to-basics-vibe
touch index.html style.css script.jsThere’s something incredibly peaceful about an empty folder.
No node_modules weighing down your hard drive.
No package.json with fifty dependencies.
Just three files and infinite possibilities. (Who’s freaking out?)
Peaceful, albeit intimidating 🥲
Related: How To Build A Killer Custom Time Input
The HTML Structure
We’re building a dashboard that tracks three things:
- a counter
- text input
- “last updated” timestamp
The catch? They all must stay in sync manually using vanilla JavaScript.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>THT: Back to Basics</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<main id="app">
<h1>Control Center 🕹️</h1>
<div class="card">
<button id="increment-btn">Boost Power: <span id="count-display">0</span></button>
</div>
<div class="card">
<input type="text" id="status-input" placeholder="Enter system status...">
<p>Live Status: <span id="status-display">Idle</span></p>
</div>
<div class="card">
<p>Last Sync: <span id="sync-time">Never</span></p>
</div>
</main>
<script src="script.js"></script>
</body>
</html>Explore: This Is How To Build Custom Pagination With JavaScript
Step 2: Implementing Manual State
Here’s where we tackle the burnout head-on.
AI often hallucinates state management logic in vanilla JS because it tries to use framework patterns where they don’t belong.
Let’s write a “single source of truth” pattern from scratch.
The goal: We want a system where the data (the “state”) lives in one place, and the UI (the “view”) simply reacts whenever that data changes.
Let’s walk through the process:
- Define the state
const state = {
count: 0,
status: 'Idle',
lastUpdated: null
};- Select DOM elements
const countDisplay = document.querySelector('#count-display');
const statusInput = document.querySelector('#status-input');
const statusDisplay = document.querySelector('#status-display');
const syncTime = document.querySelector('#sync-time');
const incrementBtn = document.querySelector('#increment-btn');- Define functions like the render function (the “reacter”) and the dispatcher (the “updater”)
function render() {
countDisplay.textContent = state.count;
statusDisplay.textContent = state.status;
syncTime.textContent = state.lastUpdated
? state.lastUpdated.toLocaleTimeString()
: 'Never';
}
function updateState(newState) {
Object.assign(state, newState);
state.lastUpdated = new Date();
render();
}- Add listeners to
clickandinputevents for connecting the UI to the logic
incrementBtn.addEventListener('click', () => {
updateState({ count: state.count + 1 });
});
statusInput.addEventListener('input', (e) => {
updateState({ status: e.target.value || 'Idle' });
});- Execute the render function initially to show the starting values
render();Tip 📝
Did you notice how we didn’t just change thetextContentinside the event listener? We updated the state first, then called a render function. This is a unidirectional data flow pattern. Understanding this prevents 90% of the bugs AI typically creates.
Explore: How To Build A Futuristic Pokémon Search App (FreeCodeCamp Challenge)
Debugging Pitstop: The “State vs. View” Trap
If you’re typing this out and nothing is happening, check your console.
A common mistake here is trying to update countDisplay.textContent directly inside the event listener 😬
Don’t do that! If you do, your state object and your UI will drift apart.
Tip: Go ahead and try to do this the wrong way to see the bug in action. Best way to learn.
Always update the state via updateState(), and let the render() function handle the UI. This “one-way data flow” is what keeps modern apps from becoming a buggy mess.
Step 3: Adding Persistence (The Real-World Test)
In a real project, you’d want this data to stay put when the user refreshes. Let’s add a manual save/load feature using localStorage.
function saveToDisk() {
localStorage.setItem('tht_dashboard_state', JSON.stringify(state));
}
function loadFromDisk() {
const saved = localStorage.getItem('tht_dashboard_state');
if (saved) {
const parsed = JSON.parse(saved);
// Date objects turn into strings in JSON, so we have to revive them!
if (parsed.lastUpdated) parsed.lastUpdated = new Date(parsed.lastUpdated);
Object.assign(state, parsed);
}
}Then update the updateState function to auto-save:
function updateState(newState) {
Object.assign(state, newState);
state.lastUpdated = new Date();
saveToDisk(); // Save every change!
render();
}Now go ahead and test it out by refreshing the browser.
It’s a Wrap
If you followed along, give yourself a pat on the back. You just did something that a lot of devs are struggling with right now.
You built a functioning system with zero dependencies and 100% intentionality 🙌
What we learned today:
- The Debugging Tax is avoidable if you build in small, verifiable batches.
- Frameworks are just tools, and knowing the “vanilla” way makes you a better debugger.
- State vs. UI: Keeping them separate is the secret to clean code.
As for your next step, don’t stop here! Take our little Control Center and add one “Manual” feature:
- Add a “Theme Toggle”: Save the user’s preference (Light/Dark) to
localStoragewithout using a library. - Add an “Undo” Button: Can you keep an array of previous states and “pop” them back?
Go ahead and try this!
Stay curious, stay intentional, and remember: You’re the pilot. The AI is just the co-pilot.
See ya 🚀