DEV Community

Cover image for Every developers first friend: Vanilla JS
Shaman Shetty
Shaman Shetty

Posted on

Every developers first friend: Vanilla JS

Introduction

I still remember the first time I wrote a line of JavaScript that actually worked. It was a simple snippet that made a button change color when clicked, but I felt like I had unlocked some kind of digital sorcery. That moment sparked my journey into web development, and while I've worked with countless frameworks since then, I keep coming back to the raw power of vanilla JavaScript.

In today's landscape of shiny frameworks and libraries, it's easy to skip straight to React, Vue, or the latest JavaScript flavor of the month. I've been guilty of this myself! But over years of debugging and building projects, I've realized something crucial: developers who master vanilla JS first simply build better applications, regardless of what framework they eventually adopt.

Let's dive into why vanilla JavaScript deserves your attention, how to set up a project from scratch, and some clever tricks that will make you appreciate the elegant simplicity of JavaScript in its purest form.

The Underrated Power of Vanilla JavaScript

When I mention "vanilla JavaScript," I'm referring to plain JavaScript without any additional libraries or frameworks. Just you and the language that powers over 97% of websites on the internet.

Here's why it remains incredibly powerful despite being over 25 years old:

1. Universal Browser Support

Every modern browser comes with a JavaScript engine built-in. No installation, no compatibility issues, no dependency nightmares. When you write vanilla JS, you're writing code that works everywhere without additional overhead.

2. Performance Benefits

I once inherited a project that loaded jQuery just to toggle a few classes and handle basic DOM manipulation. The entire library (compressed) added ~87KB to the page load, while the equivalent vanilla JS solution was less than 1KB. Frameworks add convenience but often at the cost of performance, and users notice slow websites!

3. Complete Control

With vanilla JS, there's no "magic" happening behind the scenes. Every operation is explicitly defined by you, which means you understand exactly what's happening in your code. This control becomes invaluable when debugging complex issues.

4. Future-Proof Skills

Frameworks come and go (remember AngularJS?), but core JavaScript concepts remain. The time I invested in understanding JavaScript fundamentals has paid dividends across every framework I've learned since.

Why Every Developer Should Learn Vanilla JS First

I've mentored several junior developers who jumped straight into React or Angular without learning JavaScript basics. Inevitably, they hit roadblocks that were fundamentally JavaScript problems, not framework problems.

Here's why mastering vanilla JS should be your first priority:

1. Frameworks Are Abstractions, Not Replacements

React, Angular, Vue—they're all written in JavaScript. When you use a framework without understanding vanilla JS, you're building on a foundation you don't fully understand. I've seen developers struggle for hours with issues that would be obvious with better JS knowledge.

2. Better Debugging Skills

When something breaks in a framework (and something always breaks!), debugging often requires peeling back layers of abstraction to find the core issue. With solid vanilla JS knowledge, you can quickly identify whether the problem is in your code, the framework, or elsewhere.

3. Smaller, Faster Applications

Not every project needs a heavyweight framework. Some of my most successful projects use minimal or no frameworks at all. Understanding vanilla JS lets you make informed decisions about when to use frameworks and when to go vanilla.

4. Framework Flexibility

Once you understand vanilla JS deeply, picking up new frameworks becomes significantly easier. I switched from Angular to React in a week because the fundamental JavaScript concepts remained the same—only the syntax and patterns changed.

The Core Logic of JavaScript

JavaScript has some unique characteristics that make it both powerful and occasionally frustrating. Understanding these core concepts will help you build a strong mental model:

1. Event-Driven Programming

JavaScript on the web is fundamentally event-driven. Instead of executing linearly from top to bottom, much of your code will run in response to events like clicks, form submissions, or data loading. This reactive nature is central to how JavaScript works in browsers.

2. Asynchronous Execution

One of the most powerful (and initially confusing) aspects of JavaScript is its asynchronous nature. Functions like setTimeout, Promises, and async/await allow non-blocking code execution. I spent weeks truly understanding this concept, but it transformed how I approach problems.

3. Prototype-Based Inheritance

Unlike class-based languages like Java or C#, JavaScript uses prototypal inheritance. Objects inherit directly from other objects. This approach is more flexible but requires a mental shift if you're coming from other programming paradigms.

4. The DOM API

The Document Object Model (DOM) is the interface between JavaScript and the HTML/CSS that forms your webpage. Understanding how to efficiently select, modify, and create DOM elements is crucial for web development.

Setting Up a Vanilla JS Project: Back to Basics Tutorial

Let's create a simple project using nothing but vanilla JavaScript. I'll walk you through my typical setup process:

Step 1: Create Your Project Structure

vanilla-js-project/
├── index.html
├── css/
│   └── style.css
└── js/
    └── main.js
Enter fullscreen mode Exit fullscreen mode

Step 2: Set Up Your HTML File

Create a basic HTML5 document that loads your JavaScript and CSS:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Vanilla JS Project</title>
    <link rel="stylesheet" href="css/style.css">
</head>
<body>
    <header>
        <h1>My Vanilla JS App</h1>
    </header>

    <main>
        <section class="todo-app">
            <h2>Task List</h2>
            <form id="task-form">
                <input type="text" id="task-input" placeholder="Add a new task..." required>
                <button type="submit">Add Task</button>
            </form>
            <ul id="task-list"></ul>
        </section>
    </main>

    <script src="js/main.js"></script>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

Step 3: Add Some Basic CSS

/* css/style.css */
* {
    box-sizing: border-box;
    margin: 0;
    padding: 0;
}

body {
    font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
    line-height: 1.6;
    color: #333;
    max-width: 800px;
    margin: 0 auto;
    padding: 20px;
}

header {
    text-align: center;
    margin-bottom: 40px;
}

.todo-app {
    background: #f5f5f5;
    border-radius: 5px;
    padding: 20px;
    box-shadow: 0 2px 5px rgba(0,0,0,0.1);
}

form {
    display: flex;
    margin-bottom: 20px;
}

input {
    flex: 1;
    padding: 10px;
    border: 1px solid #ddd;
    border-radius: 4px 0 0 4px;
}

button {
    padding: 10px 15px;
    background: #4caf50;
    color: white;
    border: none;
    border-radius: 0 4px 4px 0;
    cursor: pointer;
}

ul {
    list-style: none;
}

li {
    padding: 10px;
    background: white;
    margin-bottom: 10px;
    border-radius: 4px;
    display: flex;
    justify-content: space-between;
}

li button {
    background: #f44336;
    border-radius: 4px;
}

.completed {
    text-decoration: line-through;
    color: #888;
}
Enter fullscreen mode Exit fullscreen mode

Step 4: Write Your JavaScript Logic

// js/main.js
document.addEventListener('DOMContentLoaded', () => {
    // Get DOM elements
    const taskForm = document.getElementById('task-form');
    const taskInput = document.getElementById('task-input');
    const taskList = document.getElementById('task-list');

    // Load tasks from localStorage
    let tasks = JSON.parse(localStorage.getItem('tasks')) || [];

    // Render initial tasks
    renderTasks();

    // Add task event
    taskForm.addEventListener('submit', (e) => {
        e.preventDefault();

        const taskText = taskInput.value.trim();
        if (taskText === '') return;

        // Create new task object
        const task = {
            id: Date.now(),
            text: taskText,
            completed: false
        };

        // Add to tasks array
        tasks.push(task);

        // Save to localStorage
        saveTasksToStorage();

        // Render tasks
        renderTasks();

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

    // Event delegation for task list (click for complete or delete)
    taskList.addEventListener('click', (e) => {
        // If delete button clicked
        if (e.target.classList.contains('delete-btn')) {
            const taskId = parseInt(e.target.parentElement.getAttribute('data-id'));
            tasks = tasks.filter(task => task.id !== taskId);
            saveTasksToStorage();
            renderTasks();
        }
        // If task item clicked (toggle completion)
        else if (e.target.tagName === 'SPAN') {
            const taskId = parseInt(e.target.parentElement.getAttribute('data-id'));
            tasks = tasks.map(task => {
                if (task.id === taskId) {
                    return { ...task, completed: !task.completed };
                }
                return task;
            });
            saveTasksToStorage();
            renderTasks();
        }
    });

    // Save tasks to localStorage
    function saveTasksToStorage() {
        localStorage.setItem('tasks', JSON.stringify(tasks));
    }

    // Render tasks to DOM
    function renderTasks() {
        taskList.innerHTML = '';

        tasks.forEach(task => {
            const li = document.createElement('li');
            li.setAttribute('data-id', task.id);

            const span = document.createElement('span');
            span.textContent = task.text;
            if (task.completed) {
                span.classList.add('completed');
            }

            const deleteBtn = document.createElement('button');
            deleteBtn.textContent = 'Delete';
            deleteBtn.classList.add('delete-btn');

            li.appendChild(span);
            li.appendChild(deleteBtn);
            taskList.appendChild(li);
        });
    }
});
Enter fullscreen mode Exit fullscreen mode

This creates a functional todo app with the ability to add tasks, mark them as complete, delete them, and persist them to localStorage—all without a single library or framework!

Vanilla JS Hacks To Impress Your Friends

Here are some of my favorite vanilla JS techniques that demonstrate its power:

1. Using Template Literals for HTML Rendering

Instead of complex DOM manipulation, you can use template literals for cleaner HTML rendering:

function renderTasks() {
    taskList.innerHTML = tasks.map(task => `
        <li data-id="${task.id}">
            <span class="${task.completed ? 'completed' : ''}">${task.text}</span>
            <button class="delete-btn">Delete</button>
        </li>
    `).join('');
}
Enter fullscreen mode Exit fullscreen mode

This approach is remarkably similar to JSX in React, but it's pure JavaScript!

2. Event Delegation for Dynamic Elements

Instead of attaching event listeners to each button (which can cause memory leaks), use event delegation:

document.getElementById('list').addEventListener('click', (e) => {
    if (e.target.matches('.delete-btn')) {
        // Handle delete button click
    }
});
Enter fullscreen mode Exit fullscreen mode

This pattern works for elements that don't even exist yet when the code runs!

3. The Power of Array Methods

Modern JavaScript has incredible array methods that make data manipulation elegant:

// Filter items
const activeItems = items.filter(item => !item.completed);

// Transform items
const itemTexts = items.map(item => item.text);

// Find an item
const targetItem = items.find(item => item.id === targetId);

// Check if any/all items match a condition
const hasCompleted = items.some(item => item.completed);
const allCompleted = items.every(item => item.completed);
Enter fullscreen mode Exit fullscreen mode

Learning these methods transformed how I write JavaScript.

4. Using the Intersection Observer API

One of my favorite modern browser APIs, the Intersection Observer lets you efficiently detect when elements enter the viewport:

const observer = new IntersectionObserver(entries => {
    entries.forEach(entry => {
        if (entry.isIntersecting) {
            entry.target.classList.add('visible');
        }
    });
});

document.querySelectorAll('.animate-on-scroll').forEach(el => {
    observer.observe(el);
});
Enter fullscreen mode Exit fullscreen mode

I used to rely on jQuery plugins for this functionality, but vanilla JS now offers even better solutions!

Modern JavaScript Features Worth Learning

The JavaScript language has evolved dramatically in recent years. These modern features make vanilla JS more powerful than ever:

1. Destructuring Assignment

// Extract values from objects
const { name, age } = person;

// Extract values from arrays
const [first, second] = items;

// Set default values
const { name = 'Anonymous' } = user;
Enter fullscreen mode Exit fullscreen mode

2. Spread and Rest Operators

// Combine arrays
const newArray = [...array1, ...array2];

// Clone objects
const clonedObject = { ...originalObject };

// Collect remaining arguments
function sum(...numbers) {
    return numbers.reduce((total, num) => total + num, 0);
}
Enter fullscreen mode Exit fullscreen mode

3. Optional Chaining

// Before
const userName = user && user.info && user.info.name;

// After
const userName = user?.info?.name;
Enter fullscreen mode Exit fullscreen mode

4. Nullish Coalescing

// Use default only if value is null/undefined (not falsy)
const count = data.count ?? 0;
Enter fullscreen mode Exit fullscreen mode

When to Use a Framework (And When Not To)

After years of building with both vanilla JS and frameworks, here's my practical advice on when each approach makes sense:

Use Vanilla JS When:

  • Building small to medium-sized applications with limited state management needs
  • Performance is critical (especially on mobile or low-powered devices)
  • You want to minimize dependencies and keep bundle size small
  • Your application has simple DOM manipulation requirements
  • You're building a library or utility that others will use

Consider a Framework When:

  • Building large, complex applications with sophisticated state management
  • Working with a team where consistent patterns and structure are important
  • Creating an application with frequent updates to the UI based on data changes
  • The productivity benefits outweigh the performance costs
  • You need features like routing, state management, and component reusability

Conclusion

My journey with JavaScript has taught me that frameworks are tools, not shortcuts. The time I've invested in mastering vanilla JavaScript has paid off repeatedly throughout my career, enabling me to debug complex issues, optimize performance, and pick up new frameworks quickly.

Whether you're just starting your web development journey or looking to strengthen your foundation, don't skip the vanilla JS fundamentals. The knowledge you gain will serve you throughout your entire career, regardless of which frameworks you eventually adopt.

What vanilla JS techniques have you found most valuable in your own work? I'd love to hear your experiences in the comments below!

Happy coding!

Top comments (0)