Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

Server data on the page

This lesson joins two topics: asynchronous requests and working with the page. A typical task is to load a list of products or messages from a server and show it. While the data is on its way, the user should see that it is loading, and on failure — a clear message.

The site's sandbox has no network access, so in the examples the server is replaced by a function that returns a promise with data after half a second. At the end of the lesson you will see how the same code looks with a real fetch().

The loading state

While there is no response yet, the page shows a label or an indicator, and when the data arrives it is replaced with the result:

<p id="status">Loading…</p>
 
<script>
  const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
 
  async function show() {
    await wait(500);
    document.querySelector('#status').textContent = 'Data loaded';
  }
 
  show();
</script>

Rendering a list

Data from a server usually arrives as an array of objects. Elements are built from it, just as in the lesson on creating elements:

<ul id="users"></ul>
 
<script>
  function loadUsers() {
    const users = [{ name: 'Anna' }, { name: 'Oleg' }, { name: 'Maria' }];
    return new Promise((resolve) => setTimeout(() => resolve(users), 500));
  }
 
  async function showUsers() {
    const list = document.querySelector('#users');
    const users = await loadUsers();
 
    for (const user of users) {
      const item = document.createElement('li');
      item.textContent = user.name;
      list.append(item);
    }
  }
 
  showUsers();
</script>

Errors

A request can fail, and the user must not be left staring at "Loading…" forever. The error is caught with try...catch and a message is shown:

<p id="status">Loading…</p>
 
<script>
  function loadUsers() {
    return new Promise((resolve, reject) => {
      setTimeout(() => reject(new Error('The server is unavailable')), 500);
    });
  }
 
  async function showUsers() {
    const status = document.querySelector('#status');
 
    try {
      const users = await loadUsers();
      status.textContent = `Users: ${users.length}`;
    } catch (error) {
      status.textContent = `Error: ${error.message}`;
    }
  }
 
  showUsers();
</script>

Loading on a button click

Data is often loaded in response to a user action. During the request the button is turned off with the disabled property so that it is not clicked twice, and turned back on in finally:

<button id="load">Show the rate</button>
<p id="rate"></p>
 
<script>
  const button = document.querySelector('#load');
  const rate = document.querySelector('#rate');
 
  function loadRate() {
    return new Promise((resolve) => setTimeout(() => resolve(0.92), 500));
  }
 
  button.addEventListener('click', async () => {
    button.disabled = true;
    rate.textContent = 'Loading…';
 
    try {
      const value = await loadRate();
      rate.textContent = `1 $ = ${value} €`;
    } catch (error) {
      rate.textContent = 'Could not load the rate';
    } finally {
      button.disabled = false;
    }
  });
</script>

With a real fetch

In a real project a request to the server stands in place of the stand-in function. The rest of the code stays the same:

async function loadUsers() {
  const response = await fetch('/api/users');
 
  if (!response.ok) {
    throw new Error(`Error ${response.status}`);
  }
  return response.json();
}

That is why it pays to put data loading into a separate function: you can replace it without touching the code that draws the page.

Common mistake

Adding elements on every load without clearing the old ones. After the second click the list doubles. Before showing new data, clear the container with the replaceChildren() method:

<button>Refresh</button>
<ul></ul>
 
<script>
  const list = document.querySelector('ul');
 
  document.querySelector('button').addEventListener('click', () => {
    list.replaceChildren();
 
    for (const name of ['Tea', 'Coffee']) {
      const item = document.createElement('li');
      item.textContent = name;
      list.append(item);
    }
  });
</script>

Remove the replaceChildren() line and click the button a few times — the list starts to grow.

Thanks for reading!

Donate to the site — we work hard for you.