Lesson of the day: Pure functions/Side Effects/Readable Code

Lara Mo
3 min readFeb 14, 2024

Ah, February, the month of love… for JavaScript! A few of my colleagues took a peek at my PR at work, and they sprinkled it with some fantastic insights ✨

📖 Intro

Picture this: we’re in a restaurant where customers place orders via an iPad connected to an API. Here’s what an order would look like:

const table1Data = [
{
name: "Avocado Salad",
image: "🥑",
isInStock: true
},
{
name: "Carrot Salad",
image: "🥕",
isInStock: false
},
{
name: "Corn Spread",
image: "🌽"
isInStock: false
}
{
name: "Blueberry Shake",
image: "🫐",
isInStock: true
}
]

In a useEffect, our goal is to populate a variable named menuItems in the following format where the key is the image & the value is the name:

const menuItems = [
...
🌽: "Corn Spread",
🫐: "Blueberry Shake",
]

Additionally, in that same useEffect, I would like to check if at least one item is not in stock.

👩‍💻 Code — My initial solution

In my initial implementation, I used a forEach loop within a useEffect hook to process the menu items and checked if at least one item is missing from the inventory.
Take a look at this code

// will run each time an order for table1 is placed
useEffect(() => {
let isStockedState = false; // used to track the inventory state
const menuItems: MenuItems = {};
if (table1Data) {
table1Data.forEach((details) => {
menuItems[details.image] = details.name;
if (!isStockedState && !details.isInStock) {
// keep track of the stock of the items
isStockedState = true;
}
});
setMenuItems({ ...menuItems });
}
// check if at least one item is missing
if (!isStockedState) {
console.warn("at least one item is missing");
}
}, [setMenuItems, table1Data]);

🚩 This code is functional, but it has a few red flags 🚩

  1. Impure Function: The forEach loop mutates two variables; menuItems and isStockedState variable. This approach is impure because it mutates variables outside the scope of the loop. Impure functions can produce different outputs for the same input, making it challenging to test and debug.
    I discuss pure functions in this article :)
  2. Low Readability: This forLoop is in charge of a lot of logic; we are aggregating menuItems and checking stock availability making it difficult to understand and maintain.
  3. Lack of Separation of Concerns: This forLoop has multiple responsibilities. What if all of a sudden I do not want know if some menu items are out of stock? I’d have to go through the forLoop, hunting down every line related to the isStockedState variable.

The rule is to generally not to use forEach because they give to much freedom and can have some side effects with unexpected results. Instead in this case we can use some and reduce which are pure functions.

✅ New Solution

const aggregateMenuItemsByImage =
() => (acc: {[index: string]:string}, details: MenuItems) => {
return {
...acc,
[details.image]: details.name,
};
};


useEffect(() => {
if (table1Data) {
const menuItems = table1Data.reduce(aggregateMenuItemsByImage(), {});
setMenuItems({ ...menuItems });
const isInStock = table1Data.some((items) => !items.isInStock);
if (!isInStock) {
console.warn("at least one item is missing");
}
}
}, [setMenuItems, table1Data]);

The code is refactored to clearly separate the logic for aggregating menu items and checking stock availability.

  1. Using reduce
    We precisely extract the desired data from the entire array of menu items. The code is clear and predictable. reduce is a pure function that will have the same output based on the same given input.
  2. Using some
    This function determines whether any items are out of stock, clearly conveying its purpose. If we ever need to cease tracking availabilities, we’ll easily pinpoint where the logic resides in the code. This boosts maintainability, predictability, and purity.

Conclusion

This solution offers clear steps for achieving its goals. It ensures there are no side effects, as both some and reduce functions rely on specific parameters and consistently yield the same outputs for identical inputs. Consequently, the function achieves purity, contrasting with the previous implementation where forEach modified external variables.

Regarding performance considerations, while looping through large objects multiple times may raise concerns, it’s important to note that the operations within each loop are atomic. Thus, the overall efficiency remains comparable, and prioritizing readability and maintainability often outweighs marginal performance gains.

Thank you for reading, and I hope you learnt something today!
- Lara

--

--

Lara Mo

Student @Concordia University | Front-end developer @Plusgrade Passionate about React, JS, CSS, HTML and coffee :) | Coding & coffee is my moto ☕👩‍💻