JavaScript
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax and Types
-
Section 3: Strings and Numbers in Depth
-
Section 4: Control Flow
-
Section 5: Functions
-
Section 6: Objects and Arrays
-
Section 7: Maps, Sets, and Symbols
-
Section 8: Asynchronous JavaScript
-
Section 9: Object-Oriented and Prototypes
-
Section 10: The DOM
-
Section 11: Browser APIs
-
Section 12: Modern JavaScript (ES2015-ES2025)
-
Section 13: Functional Programming Patterns
-
Section 14: Error Handling and Debugging
-
Section 15: Testing
-
Section 16: Accessibility for JavaScript Developers
-
Section 17: Internationalization and Localization
-
Section 18: Performance
-
Section 19: Node.js Fundamentals
-
Section 20: Regular Expressions
-
Section 21: Design Patterns in JavaScript
-
Section 22: Security Basics
-
Section 23: Data Structures and Algorithms in JavaScript
-
Section 24: Practical Projects
-
Section 25: More Advanced Async Patterns
-
Section 26: More Object and Class Practice
-
Section 27: Working with Dates and Internationalization
-
Section 28: Web Components
-
Section 29: More DOM and Browser Practice
-
Section 30: Build Tooling for Vanilla JavaScript
-
Section 31: More Practice Projects
-
Section 32: Interview and Algorithm Practice
-
Section 33: Error Objects (MDN Reference)
-
Section 34: TypedArrays and Binary Data
-
Section 35: Reflection and Metaprogramming (MDN Reference)
-
Section 36: More Global Functions (MDN Reference)
85: The MutationObserver API
Up until now, we've mostly handled events that are triggered by users—clicks, keypresses, submits. But in a modern app, the DOM changes all the time, often because of other scripts, API responses, or third-party libraries. If you need to react when a specific element is added to the page or an attribute changes, you can't just add a click listener. You need the MutationObserver API.
I used to be the person who would just run a setInterval every 100ms to check if an element existed yet. It's a terrible habit. It kills performance and feels like a hack. MutationObserver is the professional way to handle this; it's asynchronous and only fires when actual changes happen.
Setting up a chat window to watch
Let's build something concrete. Imagine we have a chat window. We want to automatically scroll to the bottom whenever a new message is appended to the list, regardless of what piece of code actually added that message. Here is our basic structure:
<div id="chat-container" style="overflow-y: scroll; height: 200px;">
<ul id="message-list">
<li>System: Welcome to the chat!</li>
</ul>
</div>
<button id="send-btn">Send Message</button>
Defining the observer's reaction
The observer takes a callback function. This function receives a list of MutationRecord objects, which tell you exactly what changed. I don't usually need the granular detail of every single single character change, so I'll just check if any nodes were added.
const messageList = document.getElementById('message-list');
const callback = (mutationsList) => {
for (const mutation of mutationsList) {
if (mutation.type === 'childList') {
console.log('A new message arrived!');
const container = document.getElementById('chat-container');
container.scrollTop = container.scrollHeight;
}
}
};
const observer = new MutationObserver(callback);
The mistake I always make with config
Now, I need to tell the observer which element to watch and what specifically to look for. In my first attempt at this, I did this:
// This is the wrong way!
observer.observe(messageList);
And... nothing happened. I spent five minutes wondering why my callback wasn't firing. The problem is that observe() requires a configuration object as the second argument. You can't just point it at an element; you have to tell it what about that element matters. Do you care about attributes? Text content? Child elements?
Since we are looking for new <li> tags, we need to set childList: true. I'll also add subtree: true just in case the messages are wrapped in another container inside the list.
// The corrected version
const config = {
childList: true,
subtree: true
};
observer.observe(messageList, config);
Testing it in action
To make sure this actually works, let's simulate a message being added. I'm not going to use the button for this; I'll just push a new element into the DOM using a script to prove the observer is doing the heavy lifting.
// Simulating an incoming message from a websocket or API
setTimeout(() => {
const newMessage = document.createElement('li');
newMessage.textContent = 'User: Hey, is this working?';
messageList.appendChild(newMessage);
}, 2000);
Now, when that appendChild runs, the observer notices the childList change, triggers the callback, and scrolls the container. It's clean, decoupled, and efficient. Just remember: once you're done with an observer, call observer.disconnect() to prevent memory leaks, especially in single-page applications where elements are constantly being destroyed and recreated.
📋 Practical Task
Build a "Dark Mode" Attribute Watcher
Create a small project where you use a MutationObserver to watch the <body> element for changes to its class attribute.
- Create a button that toggles a
.dark-themeclass on the<body>. - Implement a
MutationObserverthat detects when theclassattribute changes. - When the observer detects the change, have it log "Theme changed to Dark" or "Theme changed to Light" to the console based on whether the class is present.
- Ensure your
MutationObserverconfiguration specifically targetsattributes.
There are no comments for now.