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?