bhylu.com

Advertisements

jQuery AJAX Cancellation

Cancel obsolete work and keep a generation check as an explicit ownership rule.

How it works

When users change a choice quickly, requests can finish out of order. Keeping only the last started request’s result avoids a stale response replacing the current view. This example aborts the earlier jqXHR and also checks a generation number before rendering. North deliberately waits longer than Garden in the fixture. Aborting does not undo a server-side business action, which is why this example uses a read-only endpoint.

Run in a separate local browser lab

Save this complete HTML source as index.html in an empty folder. Put the pinned full jQuery 4.0.0 file beside it and preserve the MIT licence notice. The document includes its own CSS and JavaScript. Bhylu’s shared website theme does not load jQuery globally.

This example requires HTTP. Save the original loopback fixture server in the same folder. With Node 22, run node lab-server.mjs there and open http://127.0.0.1:8127. Stop it with Ctrl+C. The fixed endpoints use fictional data and never save requests. File URLs do not reproduce this contract.

jQuery examples do not run in Bhylu’s current playground: it blocks external libraries and network access. No Apache, MySQL or website configuration change is needed. Review the full-build setup before using Ajax, effects or plugins.

Example

index.html

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>jQuery Prevent an Older Response From Replacing a New Choice</title>
    <style>
      :root {
        font:
          18px/1.6 system-ui,
          sans-serif;
        color: #16304e;
        background: #fff;
        color-scheme: light;
      }
      * {
        box-sizing: border-box;
      }
      body {
        margin: 0;
      }
      main {
        max-width: 50rem;
        margin: auto;
        padding: 1.25rem;
      }
      h1 {
        font-size: 1.8rem;
        line-height: 1.2;
      }
      h2 {
        font-size: 1.3rem;
      }
      button,
      input,
      select,
      textarea {
        font: inherit;
        max-width: 100%;
      }
      button {
        margin: 0.2rem;
        padding: 0.4rem 0.7rem;
        background: #034fa0;
        color: white;
        border: 1px solid #034fa0;
        border-radius: 0.3rem;
      }
      button:disabled {
        opacity: 0.6;
      }
      label {
        display: block;
        margin: 0.5rem 0;
      }
      input:not([type='checkbox']),
      select,
      textarea {
        display: block;
        padding: 0.3rem;
      }
      a {
        color: #034fa0;
      }
      output {
        display: block;
      }
      article,
      section,
      fieldset {
        padding: 0.75rem;
        margin-block: 0.75rem;
        border: 1px solid #a9bcd1;
      }
      ul {
        padding-left: 1.5rem;
      }
      :focus-visible {
        outline: 3px solid #0078d4;
        outline-offset: 3px;
      }
      .chosen {
        border: 3px solid #0078d4;
      }
      .highlight {
        font-weight: 700;
      }
      .muted {
        opacity: 0.7;
      }
      pre {
        white-space: pre-wrap;
        overflow-wrap: anywhere;
      }
      :root[data-theme='dark'] {
        color: #d6e7fb;
        background: #061d30;
        color-scheme: dark;
      }
      :root[data-theme='dark'] a {
        color: #8ecbff;
      }
    </style>
  </head>
  <body>
    <main>
      <h1>Neighbourhood book swap</h1>
      <label
        >Shelf<select id="shelf">
          <option value="walks">Walks shelf</option>
          <option value="garden">Garden shelf</option>
        </select></label
      ><output></output>
    </main>
    <script src="jquery.min.js"></script>
    <script>
      jQuery(function ($) {
        let active,
          generation = 0;
        function load() {
          const mine = ++generation;
          if (active) active.abort();
          const garden = $('#shelf').val() === 'garden';
          active = $.ajax({
            url: '/api/books',
            dataType: 'json',
            data: { kind: garden ? 'garden' : 'walks', delay: garden ? 0 : 200 },
          })
            .done(function (rows) {
              if (mine === generation)
                $('output').text(rows.map((row) => row.title).join(' / '));
            })
            .fail(function (xhr, status) {
              if (mine === generation && status !== 'abort')
                $('output').text('Could not load current shelf');
            });
        }
        $('#shelf').on('change.swap', load);
        load();
      });
    </script>
  </body>
</html>

Expected result

Switching immediately to Garden leaves Small Gardens visible after the slower earlier work would have finished.

Increment generation before aborting so a synchronous abort callback cannot claim ownership of the new choice. Real response shape validation should also match the endpoint contract; the fixture data is controlled for this focused race example. A component teardown should abort its remaining read and release handlers. A browser-side abort is a resource/lifetime choice, not a durable transaction rollback.

Use jQuery’s official API documentation for exact method and return contracts.

Advertisements