A script replacement loading pattern

👨
📅 📖 3 min read

When loading dynamic content client side, it's generally considered good practice to load the page's HTML first then the scripts, which run and update the HTML. The script tags can be at the end of the body, or in the head and deferred so they don't block the HTML loading. The final result is dynamic HTML.

Now, compare that with server side rendering. The code runs server side creating the final HTML and then that is sent to the browser. The end result is the same, just got there a different way, right? Not quite. There is a subtle difference.

When running scripts client side the script tags themselves remain in the DOM. The script content, whether inline or referenced in a separate file, is available via the DevTools, providing a kind of audit trail of how the updated HTML content got there.

One time vs ongoing

It's important to differentiate between loading something once and loading things in an ongoing way. For example, let's assume we have a product details page. It's a fixed template, which dynamically loads a title, a photo, a cost and details. That's a one time load. Once it's loaded it doesn't change.

Now let's picture a product list page. Again it's a template but the loading is not necessarily a one time thing. We might search or filter and need to load again. Even if we don't go back to the server, we'll be changing the HTML again using scripts.

One time replacement

In the case where the load is a one off event we can use a replacement pattern. Let's say we want to end up with this HTML:

<header>
  <h1>A script replacement pattern</h1>
  <time datetime="2025-12-09T16:30:00.000Z">December 9, 2025</time>
</header>

We can put this in our HTML to set a loading placeholder and then replace the content.

<header>
  <!-- placeholder content -->
  <h1>Loading...</h1>
  <time>******* **, ****</time>

  <script defer>
    // identify container
    const container = document.querySelector("h1").closest("header");

    // get dynamic content
    const getData = async () => {
      const url = "https://example.org/post/123";
      const response = await fetch(url);
      const result = await response.json();
      return result;
    };
    const post = getData();

    // create dynamic HTML
    const dynamicContent = `
      <h1>${post.title}</h1>
      <time datetime="${post.date}">${post.date.toLocaleString()}</time>
    `;

    // replace content
    container.innerHTML = dynamicContent;
  </script>
</header>

The temporary placeholder content can be used to preserve space so we don't get layout shifts.

By having the script contained in the header and then being replaced it makes the header a completely self-contained unit, like a component. This also means that if something goes wrong then it only affects this part of the page and doesn't bring everything to a stop.

By replacing the JavaScript it also kind of cleans up after itself, removing any trace of how the dynamic content got there.

Error handling

So far this is all the happy path, assuming that the fetch works and the content comes in without any issues. In production we'll need to handle any errors and replace the container's content with something appropriate, like an error message or a refresh option to try it again. It would also be good to handle JavaScript not being enabled or available.

<header>
  <noscript>This content requires JavaScript to load</noscript>

  <script>
    document.write(`
      <!-- placeholder content -->
      <h1>Loading...</h1>
      <time>******* **, ****</time>
    `);
  </script>

  <script defer>
    // identify container
    const container = document.querySelector("h1").closest("header");

    // get dynamic content
    const getData = async () => {
      const url = "https://example.org/post/123";
      const response = await fetch(url);
      if (!response.ok) return null;
      const result = await response.json();
      return result;
    };
    const post = getData();

    // create dynamic HTML
    const dynamicContent = post
      ? `
      <h1>${post.title}</h1>
      <time datetime="${post.date}">${post.date.toLocaleString()}</time>
    `
      : `
      <h1>Error</h1>
      <span>This content failed to load. <a href=".">Try again</a>.</span>
    `;
    // replace content
    container.innerHTML = dynamicContent;
  </script>
</header>