From Legacy Chaos to Modern Vue: Tracking Buttons and Event Nightmares
Clicking one Track button changed the state of every other Track button on the page. A global jQuery function was querying all buttons -- including Vue-controlled ones.
Clicking a "Track" button on one agenda item changed the state of Track buttons on other items. Sometimes buttons toggled between "Track" and "Untrack" without being clicked. The bug lived at the boundary between Democracy.cc's legacy jQuery code and the newer Vue components.
The Root Cause
A global updateButtonStates() function ran on every track/untrack action. It queried all .track-btn and .untrack-btn elements on the page:
function updateButtonStates(itemId, isTracking) {
// This catches ALL buttons with these classes -- including Vue components!
document.querySelectorAll(`.track-btn[data-item="${itemId}"]`).forEach(btn => {
btn.style.display = isTracking ? 'none' : '';
});
document.querySelectorAll(`.untrack-btn[data-item="${itemId}"]`).forEach(btn => {
btn.style.display = isTracking ? '' : 'none';
});
}
Vue components used the same CSS classes. When jQuery toggled their visibility, Vue's reactive state didn't know -- leading to visual state that contradicted the component's internal state. On the next Vue re-render, things got worse.
The Fix
Added a data-vue-controlled attribute to Vue-managed buttons and excluded them from the global function:
function updateButtonStates(itemId, isTracking) {
document.querySelectorAll(`.track-btn[data-item="${itemId}"]`).forEach(btn => {
if (btn.hasAttribute('data-vue-controlled')) return; // Skip Vue buttons
btn.style.display = isTracking ? 'none' : '';
});
}
Vue components manage their own state through reactivity. jQuery manages legacy buttons. The data-vue-controlled attribute is the boundary.
The Lesson
When migrating from jQuery to a reactive framework, the most dangerous bugs live at the boundary. Global DOM queries that worked fine when everything was jQuery will silently corrupt reactive component state. The fix isn't to remove jQuery all at once -- it's to draw a clear boundary and make each side respect it.
Built while working on Democracy.cc.
Back to danielships ->