Hello! Good to see you.

This week, we are going to deal with asynchronous rendered templates, and their nature of having data that we can never assume to be there. But there are also a few more hidden difficulties.

The typical front-end developer dillemma: How can I be sure to expect the correct data? How can I make sure I get what I want?

Read on to find out how we are going to deal with these problems and go beyond!

The Code

The original code was not clean at all, also in terms of indent levels. I had to do a bit of reindenting (thanks vscode!) to make it clean.

17 Lines without comments and spaces and without expanding the HTML.

$.getJSON('https://www.googleapis.com/books/v1/volumes?q=free', function(data) {
    //Declaring variables and setting them to null
    //Asign fetched data to variables

    for (i = 0; i = data.items.length; i++) {
        var bk_link = data.items[i].volumeInfo.canonicalVolumeLink;
        var bk_id = data.items[i].id;
        var bk_title = data.items[i].volumeInfo.title;
        var bk_subtitle = data.items[i].volumeInfo.subtitle;
        var bk_img = data.items[i].volumeInfo.imageLinks.smallThumbnail;
        var bk_descr = data.items[i].volumeInfo.description;
        var bk_pages = data.items[i].volumeInfo.pageCount;

        var div = $('<div class="col-lg-3 gbooks">' + '<strong><h4 style="color:#a01b1b;" class="title">' + bk_title + '</h4></strong>' + '<img class="icon" src="' + bk_img + '" alt="">' + '<em><p class="subtitle">' + bk_subtitle + '</p></em>' + '</div>');

        var div1 = $('<div class="col-lg-6 gbooks">' + '<p class="descr">' + bk_descr + '</p>' + '</div>');

        var div2 = $('<div class="col-lg-3 gbooks">' + '<p class="b_id">Book id:' + bk_id + '</p>' + '<p class="bk_pages">No.of Pages:' + bk_pages + ' </p>' + '<a class="bk_link btn btn-danger" href="' + bk_link + '" >Read More</a>' + '</div>');
        $('#fetched_books').append(div);
        $('#fetched_books').append(div1);
        $('#fetched_books').append(div2);
    })
})

If there is one thing you can do to make your life easier: Get to know your tools. Get used to using them, and find ways to automate away the repeated work.

It almost feels like most developers are not even aware of vscode, eslint, prettier, editorconfig and similar. It may look intimidating the first time using eslint, but it pays off in less time spent with deciding every single time when writing code, how to indent, how to write commas, etc.

If you have a sharp mind, you will also see there is a dependency on jQuery, but not necessarily a required one.

Without bothering too much with the classes that are used in the HTML, below you can see how the user interface might look like:

Lets go through the code together, see what issues the developer encountered and how we can deal with them.

Our enemy: undefined variables

Before we dive any deeper, get to know your data source! It always pays off to see how the exact JSON looks like you are going to deal with: https://www.googleapis.com/books/v1/volumes?q=free

Why do we get the error in the first place?

Uncaught TypeError: Cannot read property 'volumeInfo' of undefined

If this is a no-brainer for you, you can skip to the next chapter.

This is the line that could cause it:

data.items[i].volumeInfo.canonicalVolumeLink

It is important to understand, where exactly the problem is arising. 'volumeInfo' of undefined means that data.items[i] was already undefined. Not canonicalVolumeLink! How is this possible?

In this case, the developer misunderstand how the for loop works: for (i = 0; i = data.items.length; i++)

The second statement is actually controlling the condition. But i is set to the length of the items. We fetch 10 books, and the 10th index of the array data.items does not exist.

The for loop should actually be written like this: for (i = 0; i < data.items.length; i++)

And i has to be lesser than the length of the items, not equals to. Even with == it would go wrong, because the 10th index does not exist.

This is too much mental overhead for a simple loop. There has to be a better way, where way fewer things can go wrong.

  • No maintaining an iteration count
  • No hoping or checking that the Nth element exists
  • No guessing how the order of parameters are in the for statement

No, just plain functions where nothing can go wrong, as long as you have an array. And even if the array length is zero, it will just work as well. There is .forEach if we do not need a return value. But since we want to render HTML, it is best to go with .map.

const html = data.items.map(item => {
    // data.items[i].volumeInfo.title
    return `<h4>${item.volumeInfo.title}</h4>`
})

If you still need an index, you can add a second parameter to the function:

data.items.map((item, index) => {})

Note

But there is also a reason why multiple tools and approaches exist to do the same thing. .forEach() and .map() have the drawback that you cannot abort the loop. There is no break keyword in these functions, which you might need if you have special conditions that can

While reading into how break works, I found that I am probably using only 5% of the syntax JavaScript has to offer. Remarkable how with only a few methods you can also get the job done.

There are multiple ways to get there. The most redundant way is to ask every single attribute:

item && item.volumeInfo && item.volumeInfo.imageLinks && item.volumeInfo.imageLinks.smallThumbnail

Nobody wants to work like that. There has to be a better way.

Function parameters have very special abilities (pretty much the same variable assignments have anyway). Good we use a function within .map(). With destructuring assignments we can define default values and extract only what we need. If the property does not exist, it will be just undefined.

data.items.map(({ id, volumeInfo: { title, subtitle, description, pageCount, canonicalVolumeLink, imageLinks: { smallThumbnail } = {} } = {} }) => {
    return `html...`
}

The destructuring syntax allows us to place an inline default parameter. I set volumeInfo to {} if it does not exist, which means that you won't ever get an error.

  • Every information is super short to access
  • No more need to re-alias variables
  • Same variable names as source
  • No undefined errors

Strings and HTML

To be honest, pushing strings to the DOM is not the best idea, but it works for smaller projects. I already talked about this in Component oriented UI development

TIP

Front-end frameworks such as VueJS make this extremely easy, compared to vanilla JS, because keeping state in sync with the DOM is a difficult task. If you want to make it render efficiently, you should also avoid using innerHTML, but for our example, it is okay. The right way is to use document.createElement but that is too cumbersome for this little demo.

However, we can use template literals to make preparing HTML easier. Lets create a function that creates the HTML for just one book:

function renderBook({ id, volumeInfo: { title, subtitle, description, pageCount, canonicalVolumeLink, imageLinks: { smallThumbnail } = {} } = {} }) {
    return `
        <div class="col-lg-3 gbooks">
            <strong><h4 style="color:#a01b1b;" class="title">${title}</h4></strong>
            <img class="icon" src="${smallThumbnail}" alt="">
            <em><p class="subtitle">${subtitle}</p></em>
        </div>
        <div class="col-lg-6 gbooks">
            <p class="descr">${description}</p>
        </div>
        <div class="col-lg-3 gbooks">
            <p class="b_id">Book id: ${id}</p>
            <p class="bk_pages">No.of Pages: ${pageCount}</p>
            <a class="bk_link btn btn-danger" href="${canonicalVolumeLink}">Read More</a>
        </div>
    `
}

Reducing indent levels

As you can see in the original code, we get an extra indent level for the callback function and one more with the for-loop. Indent levels are evil, they are the root of unreadable code and unmaintainable complexity. The only acceptable indent levels are for try/catch blocks, function definitions and nested objects. We will discuss that in-depth another time.

Instead of one big block that takes care of everything, we can create smaller re-usable and adaptable functions. Lets create an async function for retrieving the data:

async function getBooks(query) {
    const response = await fetch(`https://www.googleapis.com/books/v1/volumes?q=${query}`)
    const { items } = await response.json()
    return items
}

I use fetch in this example - a browser-builtin function to eliminate the jQuery dependency.

we can only await async functions in other async functions. You only need to await if you want a function to happen after the call. Lets deal with the last problem before we put everything together.

Asynchronous Errors - What if we get no result at all?

The web is very error prone, servers can go offline anytime, mobile users in the subway can lose connection. So we have to deal with these kind of problems. We have to define a level where we want to deal with different problems. Every part of your application can be in a state of loading, empty result, successful result or error. And this can be reflected in every single asynchronous part. You don't want to leave your user unknowing what exactly is happening. Ideally, you give them a way of dealing with the problem. Such as retry, change your search filters to more likely get a result or reduce the amount of results.

We can deal with the four states on different layers:

async function renderBooks(element, query, renderer) {
    element.innerHTML = 'loading ...'
    try {
        let books = await getBooks(query)
        if (!books.length) return element.innerHTML = 'no books found'
        return element.innerHTML = books.map(renderer)
    } catch(error) {
        element.innerHTML = `couldn't fetch books`
    }
}

Now all together:

async function renderBooks(element, query, renderer, getBooks) {
    element.innerHTML = 'loading ...'
    try {
        let books = await getBooks(query)
        if (!books.length) return element.innerHTML = 'no books found'
        return element.innerHTML = books.map(renderer)
    } catch(error) {
        element.innerHTML = `couldn't fetch books`
    }
}

async function getBooks(query) {
    const response = await fetch(`https://www.googleapis.com/books/v1/volumes?q=${query}`)
    const { items } = await response.json()
    return items
}

function renderBook({ id, volumeInfo: { title, subtitle, description, pageCount, canonicalVolumeLink, imageLinks: { smallThumbnail } = {} } = {} }) {
    return `
        <div class="col-lg-3 gbooks">
            <strong><h4 style="color:#a01b1b;" class="title">${title}</h4></strong>
            <img class="icon" src="${smallThumbnail}" alt="">
            <em><p class="subtitle">${subtitle}</p></em>
        </div>
        <div class="col-lg-6 gbooks">
            <p class="descr">${description}</p>
        </div>
        <div class="col-lg-3 gbooks">
            <p class="b_id">Book id: ${id}</p>
            <p class="bk_pages">No.of Pages: ${pageCount}</p>
            <a class="bk_link btn btn-danger" href="${canonicalVolumeLink}">Read More</a>
        </div>
    `
}

// call renderBooks, as part of a search, or just when loading the website:
renderBooks(document.querySelector('#books'), 'cuisine', renderBook, getBooks)

That's it!

And with that we are now at a whopping 37 lines 😐. To be fair:

  • We cleaned up the HTML
  • We deal with errors and empty results
  • The function is re-usable

But most importantly, we have created an abstraction layer, where we can just re-use the entire codebase in every other part of the application.

If we want to re-use getBooks, but with another HTML, we can do that! Just by passing a different renderer function.

If we want to render books from both google's and our database, we can do that! Just by using a different getBooks mechanism that unifies both data sources.

If we want smaller functions, we can still create one by prefilling all the attributes that we already know of.

Ready for more clear code?

Clean code is only the first symptom of a properly working application.
Send me your adventures with troublesome code
and get featured in Refactor Friday!

Join the mailinglist :)