Code review · Node.js
Review a jQuery Shopping Cart Script
Easy Code review
This script runs on an older e-commerce site's cart page. It reloads the cart every 30 seconds and lets users remove items.
Review it: list the problems, what users would notice, and how to fix each one.
The code to review
There are 5 issues to find.
function loadCart() {
$.ajax({
url: "/cart.json",
async: false,
success: function (cart) {
var html = "";
for (var i = 0; i < cart.items.length; i++) {
var item = cart.items[i];
html += "<li>" + item.name + " <a href='#' class='remove' data-id='" + item.id + "'>remove</a></li>";
}
$("#cart").html(html);
$(".remove").click(function () {
$.post("/cart/remove", { id: $(this).data("id") });
loadCart();
});
}
});
}
setInterval(loadCart, 30000);
loadCart();
Write your review
Loading the editor… (JavaScript is required to run code in your browser.)
The issues and how to fix them
Show the issues
1. XSS: item names are inserted as HTML
Critical, line 9, 11
Item names (which can include user-supplied text such as gift messages or marketplace listings) are concatenated into HTML and inserted with .html(). A name containing <img onerror=...> runs script.
Fix: Build elements with $("<li>").text(item.name) and set attributes with .attr() / .data(), or escape the values.
2. Synchronous AJAX freezes the page
Major, line 4
async: false blocks the browser's main thread for the whole request: the page cannot scroll or respond to clicks, every 30 seconds. Browsers deprecate synchronous XHR on the main thread.
Fix: Remove async: false and work with the callback or the returned promise ($.getJSON(...).done(...)).
3. Click handlers pile up
Major, line 13-16
Each refresh attaches another click handler. Items that survive a refresh get one more handler each time, so one click eventually fires several remove requests.
Fix: Bind once with event delegation: $("#cart").on("click", ".remove", handler) outside loadCart.
4. Reload races the remove request
Major, line 14-15
loadCart() runs immediately, before /cart/remove has completed, so the item often reappears until the next refresh. Errors from the POST are ignored.
Fix: Reload in the POST's success callback ($.post(...).done(loadCart).fail(showError)), and call event.preventDefault() so href='#' does not jump to the top.
5. Polling and missing CSRF protection
Minor, line 14, 21
The POST sends no CSRF token, and polling every 30 seconds keeps running in background tabs.
Fix: Send the CSRF token header with $.ajaxSetup, and refresh after user actions (or pause polling when document.hidden).
jQuery reviews come up when teams maintain legacy front ends. The classic findings are HTML built from data (XSS), handlers bound in a loop or refresh (duplicates, leaks) and synchronous AJAX. Event delegation is the jQuery idiom interviewers want to hear.
Fixed version
$.ajaxSetup({ headers: { "X-CSRF-Token": $("meta[name=csrf-token]").attr("content") } });
function renderCart(cart) {
var $list = $("#cart").empty();
cart.items.forEach(function (item) {
$("<li>")
.text(item.name + " ")
.append($("<a href='#' class='remove'>remove</a>").attr("data-id", item.id))
.appendTo($list);
});
}
function loadCart() {
return $.getJSON("/cart.json").done(renderCart);
}
// One delegated handler for current and future items.
$("#cart").on("click", ".remove", function (event) {
event.preventDefault();
$.post("/cart/remove", { id: $(this).data("id") })
.done(loadCart)
.fail(function () { alert("Could not remove the item."); });
});
setInterval(function () { if (!document.hidden) loadCart(); }, 30000);
loadCart();
Follow-up questions
- How would you migrate this page off jQuery gradually?
- What does
.off()do and when would you use namespaced events?
More Code Clinic: Code Review problems
- Review a Laravel Orders Controller · Medium
- Review an Express User Search Endpoint · Easy
- Review a Payment Webhook Handler · Medium
- Review a Sign-Up Function · Medium
- Review a CSV Export Endpoint · Medium
- Review a React Search Component · Medium