Read other articles

Improving code readability

~8 min read  ⦁  Updated on Jan 21 2024

Summary: 11 tips for improving code readability


In this article
  1. 0. Giving meaningful names to variables and functions
  2. 1. Writing booleans in the affirmative form
  3. 2. Avoiding the dot notation in conditional rendering
  4. 3. Prefer function declaration to function expressions and arrow functions
  5. 4. Have the function caller right after the callee
  6. 5. Avoid variable/function hoisting
  7. 6. Avoid complex anonymous callbacks
  8. 7. Internalize one-off functions
  9. 8. Avoid unnecessary conditional comparisons
  10. 9. Avoid using the ternary operator for chained conditionals
  11. 10. Declare more variables
  12. Conclusion

I believe that writing code can, in a way, be compared to telling a story where the order of the events matter and the role of each character has to be well thought out in order to make it engaging and understandable.

So, in this article I’ll share some of the coding practices I believe to be helpful to write predictable, engaging and easy to understand JavaScript codebases.

0. Giving meaningful names to variables and functions

Nothing worst that opening a file and finding a variable called cats as an abbreviation for categories or fubr as an abbreviation for filterUsersByRegion. This are edge cases but not far from reality.

// ⚠️
const categories = [...];
const selected = [...];
function handleClick(event) {...};

The example above shows variables and a function that are correctly written but that lack context. One may argue that the file name and utilization of the variable/function provides the needed context. But what if we could reduce the developer’s cognitive load by naming variables and functions more thoroughly?

// ✅
const productCategories = [];
const userSelectedCategories = [];
function addOrRemoveACategory(event) {...}

Now it is easy to understand that…

1. Writing booleans in the affirmative form

Variables holding boolean values become more intuitive when in the affirmative form since they are a statement and not a question.

const users = [...];
const onlineUsers = users.filter(user => user.status === "online");

// ❌
const areThereOnlineUsers = onlineUsers.length > 0;

// ✅
const thereAreOnlineUsers = onlineUsers.length > 0;

This prevents cognitive gymnastics like

// Hard to infer whether there are users or not
if (areThereOnlineUsers) {...}

in favour of much cleaner code such as

if (thereAreOnlineUsers) {...}

2. Avoiding the dot notation in conditional rendering

I believe to be easier to understand the condition driving the render inside JSX if the condition is extracted outside of it, and expressed as boolean variable instead of verifying the condition within it (the JSX).

// ❌
return (
  <>
    {
      users.length > 0
        ? <ul>
            { users.map(user => <li key={user.id}>{user.name}</li>)}
          </ul>
        : null
    }
  </>
)

// ✅
const thereAreUsers = users.length > 0;
return (
  <>
    {
      thereAreUsers
        ? <ul>
            { users.map(user => <li key={user.id}>{user.name}</li>)}
          </ul>
        : null
    }
  </>
)

3. Prefer function declaration to function expressions and arrow functions

I believe that using the traditional function declaration (if possible) reduces the cognitive load when trying to understand whether a const contains a value (primitive/object) or a functio, since the function keyword declares right from the start what we will find next.

// ⚠️
const generateReport = function() {...};
// ⚠️
const generateReport = () => {...};

// ✅
function generateReport() {...}

4. Have the function caller right after the callee

This approach alleviates the need of CTRL + Click (or gd if you Vim) to check the logic driving the function, and improves the structure of the file.

// ⚠️
function getUsers() {...}
function filterUsers(usersCollection, country) {...}
/* insert lines and lines of code */
const users = getUsers();
const mozambicanUsers = filterUsers(users, "MZ");

// ✅
function getUsers() {...}
const users = getUsers();
function filterUsers(usersCollection, country) {...}
const mozambicanUsers = filterUsers(users, "MZ");
/* insert lines and lines of code */

5. Avoid variable/function hoisting

This resource of the language allows you to access variables and functions before they are declared. This can lead to unexpected errors, and is not generally recommended. And even if this was no issue, the reading of a file with hoisted variables is never top-down and f-shaped.

// ❌
const users = getUsers();
/* insert lines of code */
function getUsers() {...};

// ✅
function getUsers() {...};
const users = getUsers();

6. Avoid complex anonymous callbacks

With ES6 the arrow functions and new Array methods (map, filter, reduce, etc) combination became a “standard” when interacting with object collections. Unfortunately, we, developers, started writing complex logic inside callbacks these methods take as arguments. That has led to convoluted, and hard to debug codebases.

// ❌
const users = [...];
const onlineUsersInEurope = users.filter(users => users.status === "online" && users.region === "EU");

In this example, we have scan all the way to the end of the line to understand how the onlineUsersInEurope are being computed.

// ✅
const users = [...];
function filterByStatusAndRegion(user) {
  return user.status === "online" && user.region === "EU"
};
const onlineUsersInEurope = users.filter(filterByStatusAndRegion);

While the code is virtually the same, it became much easier to understand how the onlineUsersInEurope are filtered.

7. Internalize one-off functions

Functions represent blocks of code that execute a certain task. Usually functions are declared so that the functionality they encompass can be accessed without repeating the code within them. But sometimes functions are only called once.

// ⚠️
/* insert lines of code */
const users = [...];
function filterOnlineUsers(user) {...};
function groupOnlineUsersByRegion(usersCollection, region) {
  const onlineUsers = usersCollection.filter(filterOnlineUsers);
  // grouping code here
};
const groupedUsers = groupOnlineUsers(users, region);
/* insert lines of code */

In the example, the filterOnlineUsers function is located in the outer scope despite only being called by the groupOnlineUsersByRegion function. This pollutes the global namespace, and misses out on the opportunity to make the code a little bit more succinct. Here’s a better approach.

// ✅
/* insert lines of code */
const users = [...];
function groupOnlineUsersByRegion(usersCollection, region) {
  function filterOnlineUsers(user) {...};
  const onlineUsers = usersCollection.filter(filterOnlineUsers);
  // grouping code here
};
const groupedUsers = groupOnlineUsers(users, region);
/* insert lines of code */

By moving the filterOnlineUsers function inside the groupOnlineUsersByRegion we remove it from the global namespace, and create it only when the parent function is created and contextualize the code execution. This is also called a closure. Check this MDN link to learn more.

8. Avoid unnecessary conditional comparisons

If there is no further action to be taken after the comparison, and the function is expected to return a boolean value, just return it.

const users = [...];

// ❌
function filterByStatus(user) {
  if (user.status === "online") return true;
  return false;
};
const onlineUsers = users.filter(filterByStatus);

// ✅
function filterByStatus(user) {
  return user.status === "online";
};
const onlineUsers = users.filter(filterByStatus);

9. Avoid using the ternary operator for chained conditionals

There is a misconception among developers that promotes the idea that “the more complex your codebase is, the smarter you are. This leads to unintelligible codebases that confuse themselves and future contributors. Coding is like communication, it’s not what we write (and perceive as clear) but what others understand after reading our code.

// ❌
const users = [...];

function filterByStatus(user) {
  return user.status === "online";
};
function filterByRegion(user) {
  return user.region === "EU";
};

const thereAreEligibleUsers = users.length > 0 ? users.filter(filterByStatus).length > 0 ? users.filter(filterByStatus).filter(filterByRegion).length > 0 ? true : false : false : false;

Did you manage to follow to code until the end 😄? Take a look at a better approach.

// ✅
const users = [...];

function filterByStatus(user) {
  return user.status === "online";
};
function filterByRegion(user) {
  return user.region === "EU";
};

if (users.length > 0) {
  if (users.filter(filterByStatus).length > 0) {
    return users.filter(filterByStatus).filter(filterByRegion).length > 0;
  } return false;
} return false;

This is much easier to follow and understand when compared to that mumbo jumbo of a one-liner we saw before.

10. Declare more variables

Variables are not “free”. They are part of the memory our programs use. But contrary to the popular belief (or at least I think), most programming languages do an amazing job in garbage collecting them . Some, like Rust, do not even have a garbage collector. Therefore, we should use as many variables as we need towards simplifying our code and prevent the rerun of expensive calculations. We’ll use the previous snippet.

// ✅
const users = [...];

function filterByStatus(user) {
  return user.status === "online";
};
function filterByRegion(user) {
  return user.region === "EU";
};

const let = false;

if (users.length > 0) {
  const onlineUsers = users.filter(filterByStatus);
  if (onlineUsers.length > 0) {
    const onlineUsersInEurope = onlineUsers.filter(filterByRegion);
    return onlineUsersInEurope.length > 0;
  } return false;
} return false;

Imagine that the users array had more than 10000 items, filtering the online users from it twice would be an unnecessary computation if we simply memoized the value of the first computation.

Conclusion

I hope you enjoyed the read and managed to pick something up for your daily coding activities.


Did you find a typo, or would like to contribute to the article? Here's the Github Link for this article.

Share this article: Twitter | Linkedin