DEV Community

Cover image for 10 Essential JavaScript Refactoring Techniques for Cleaner, Efficient Code
Aarav Joshi
Aarav Joshi

Posted on

10 Essential JavaScript Refactoring Techniques for Cleaner, Efficient Code

As a best-selling author, I invite you to explore my books on Amazon. Don't forget to follow me on Medium and show your support. Thank you! Your support means the world!

JavaScript refactoring is a crucial skill for developers looking to improve their code quality and maintainability. As I've worked on numerous projects over the years, I've found that implementing these techniques consistently leads to more robust and efficient codebases.

Code Smell Detection is often the first step in the refactoring process. I rely on tools like ESLint and SonarQube to identify potential issues in my code. These tools help me spot problems like unused variables, complex functions, and inconsistent styling. Here's an example of how I set up ESLint in my projects:

// .eslintrc.js
module.exports = {
  env: {
    browser: true,
    es2021: true,
    node: true,
  },
  extends: ['eslint:recommended', 'plugin:react/recommended'],
  parserOptions: {
    ecmaFeatures: {
      jsx: true,
    },
    ecmaVersion: 12,
    sourceType: 'module',
  },
  plugins: ['react'],
  rules: {
    // Custom rules
    'no-unused-vars': 'error',
    'max-len': ['error', { code: 100 }],
    'complexity': ['error', 10],
  },
};
Enter fullscreen mode Exit fullscreen mode

Once I've identified potential issues, I move on to specific refactoring techniques. The Extract Method is one of the most common and effective techniques I use. It involves breaking down large, complex functions into smaller, more focused ones. This not only improves readability but also makes the code more reusable and easier to test.

Consider this example of a function that calculates the total price of items in a shopping cart:

function calculateTotalPrice(items) {
  let total = 0;
  for (let item of items) {
    let price = item.price;
    if (item.onSale) {
      price *= 0.9;
    }
    if (item.quantity > 5) {
      price *= 0.95;
    }
    total += price * item.quantity;
  }
  return total;
}
Enter fullscreen mode Exit fullscreen mode

We can refactor this using the Extract Method technique:

function calculateTotalPrice(items) {
  return items.reduce((total, item) => total + calculateItemPrice(item), 0);
}

function calculateItemPrice(item) {
  let price = applyDiscounts(item.price, item);
  return price * item.quantity;
}

function applyDiscounts(price, item) {
  if (item.onSale) price *= 0.9;
  if (item.quantity > 5) price *= 0.95;
  return price;
}
Enter fullscreen mode Exit fullscreen mode

This refactored version is more readable and allows for easier testing of individual components.

Another powerful technique I frequently employ is Replace Conditional with Polymorphism. This approach is particularly useful when dealing with complex conditional logic that varies based on an object's type. Instead of using if-else statements or switch cases, we can use object-oriented principles to create a more flexible and extensible solution.

Here's an example of how I might refactor a function that calculates shipping costs based on the type of product:

// Before refactoring
function calculateShippingCost(product) {
  if (product.type === 'book') {
    return product.weight * 0.5;
  } else if (product.type === 'electronics') {
    return product.weight * 0.8 + 2;
  } else if (product.type === 'clothing') {
    return product.weight * 0.3;
  }
  return product.weight * 0.6; // Default shipping cost
}

// After refactoring
class Product {
  constructor(weight) {
    this.weight = weight;
  }

  calculateShippingCost() {
    return this.weight * 0.6;
  }
}

class Book extends Product {
  calculateShippingCost() {
    return this.weight * 0.5;
  }
}

class Electronics extends Product {
  calculateShippingCost() {
    return this.weight * 0.8 + 2;
  }
}

class Clothing extends Product {
  calculateShippingCost() {
    return this.weight * 0.3;
  }
}

// Usage
const book = new Book(2);
console.log(book.calculateShippingCost()); // 1

const electronics = new Electronics(3);
console.log(electronics.calculateShippingCost()); // 4.4
Enter fullscreen mode Exit fullscreen mode

This polymorphic approach makes it easier to add new product types without modifying existing code, adhering to the Open/Closed Principle.

The Introduce Parameter Object technique is particularly useful when dealing with functions that take many parameters. By grouping related parameters into a single object, we can simplify function signatures and make our code more maintainable.

Here's an example of how I might refactor a function that creates a user account:

// Before refactoring
function createUser(firstName, lastName, email, password, birthDate, country, city, zipCode) {
  // User creation logic
}

// After refactoring
function createUser(userDetails, address) {
  // User creation logic
}

// Usage
createUser(
  { firstName: 'John', lastName: 'Doe', email: 'john@example.com', password: 'securepass', birthDate: '1990-01-01' },
  { country: 'USA', city: 'New York', zipCode: '10001' }
);
Enter fullscreen mode Exit fullscreen mode

This refactored version is not only more readable but also more flexible, as it's easier to add or remove fields without changing the function signature.

Removing Duplicate Code is a critical aspect of refactoring that I always keep in mind. Duplicated code can lead to inconsistencies and make maintenance more difficult. I often extract common functionality into shared functions or modules.

Here's an example of how I might refactor duplicated code in a React component:

// Before refactoring
function ProductList({ products }) {
  return (
    <div>
      {products.map(product => (
        <div key={product.id}>
          <h2>{product.name}</h2>
          <p>Price: ${product.price}</p>
          <p>In stock: {product.inStock ? 'Yes' : 'No'}</p>
        </div>
      ))}
    </div>
  );
}

function FeaturedProducts({ featuredProducts }) {
  return (
    <div>
      <h1>Featured Products</h1>
      {featuredProducts.map(product => (
        <div key={product.id}>
          <h2>{product.name}</h2>
          <p>Price: ${product.price}</p>
          <p>In stock: {product.inStock ? 'Yes' : 'No'}</p>
        </div>
      ))}
    </div>
  );
}

// After refactoring
function ProductCard({ product }) {
  return (
    <div>
      <h2>{product.name}</h2>
      <p>Price: ${product.price}</p>
      <p>In stock: {product.inStock ? 'Yes' : 'No'}</p>
    </div>
  );
}

function ProductList({ products }) {
  return (
    <div>
      {products.map(product => (
        <ProductCard key={product.id} product={product} />
      ))}
    </div>
  );
}

function FeaturedProducts({ featuredProducts }) {
  return (
    <div>
      <h1>Featured Products</h1>
      {featuredProducts.map(product => (
        <ProductCard key={product.id} product={product} />
      ))}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

This refactoring eliminates duplication and makes the code more maintainable and consistent.

Renaming Variables and Functions is a simple yet powerful technique that I use frequently. Clear, descriptive names can significantly enhance code comprehension and serve as a form of self-documentation. I always strive to use names that accurately describe the purpose or behavior of the variable or function.

For example, instead of:

function calc(a, b) {
  return a * b;
}
Enter fullscreen mode Exit fullscreen mode

I would refactor to:

function calculateArea(width, height) {
  return width * height;
}
Enter fullscreen mode Exit fullscreen mode

This simple change makes the function's purpose immediately clear without needing additional comments.

Lastly, Simplifying Complex Expressions is a technique I use to make my code more readable and maintainable. Breaking down complex logical expressions into smaller, more manageable parts using intermediate variables or functions can greatly improve code clarity.

Here's an example of how I might refactor a complex condition:

// Before refactoring
if (user.age >= 18 && user.country === 'USA' && (user.state === 'California' || user.state === 'New York') && user.agreedToTerms) {
  // Allow access
}

// After refactoring
function isAdult(user) {
  return user.age >= 18;
}

function isFromUSA(user) {
  return user.country === 'USA';
}

function isFromEligibleState(user) {
  const eligibleStates = ['California', 'New York'];
  return eligibleStates.includes(user.state);
}

function hasAgreedToTerms(user) {
  return user.agreedToTerms;
}

if (isAdult(user) && isFromUSA(user) && isFromEligibleState(user) && hasAgreedToTerms(user)) {
  // Allow access
}
Enter fullscreen mode Exit fullscreen mode

This refactored version is much easier to read and understand, and each condition can be tested independently.

In my experience, consistently applying these refactoring techniques leads to significant improvements in code quality. It results in code that is easier to understand, maintain, and extend. However, it's important to remember that refactoring is an ongoing process. As requirements change and new features are added, we need to continually review and refactor our code to keep it clean and efficient.

One of the key benefits I've found from regular refactoring is that it makes adding new features or fixing bugs much easier. When code is well-structured and easy to understand, it's much quicker to locate the area that needs to be changed and make the necessary modifications without introducing new bugs.

Moreover, refactoring often leads to performance improvements. By simplifying complex logic and removing redundancies, we can often make our code run faster and use fewer resources. This is particularly important in JavaScript, where performance can have a significant impact on user experience, especially in browser environments.

Another aspect of refactoring that I find particularly valuable is how it facilitates knowledge sharing within a team. When code is clean and well-structured, it's easier for other developers to understand and work with. This leads to better collaboration and can significantly reduce the time needed for onboarding new team members.

It's also worth noting that while these techniques are powerful, they should be applied judiciously. Over-engineering can be just as problematic as under-engineering. The goal is to find the right balance - to make the code as simple and clear as possible, but no simpler.

In conclusion, mastering these JavaScript refactoring techniques has been invaluable in my career as a developer. They've helped me write better code, work more efficiently, and collaborate more effectively with my teams. While it can sometimes be tempting to rush through development to meet deadlines, I've found that taking the time to refactor regularly pays off in the long run. It leads to more robust, maintainable codebases that can adapt to changing requirements and scale as projects grow. As we continue to push the boundaries of what's possible with JavaScript, these refactoring techniques will remain essential tools in every developer's toolkit.


101 Books

101 Books is an AI-driven publishing company co-founded by author Aarav Joshi. By leveraging advanced AI technology, we keep our publishing costs incredibly low—some books are priced as low as $4—making quality knowledge accessible to everyone.

Check out our book Golang Clean Code available on Amazon.

Stay tuned for updates and exciting news. When shopping for books, search for Aarav Joshi to find more of our titles. Use the provided link to enjoy special discounts!

Our Creations

Be sure to check out our creations:

Investor Central | Investor Central Spanish | Investor Central German | Smart Living | Epochs & Echoes | Puzzling Mysteries | Hindutva | Elite Dev | JS Schools


We are on Medium

Tech Koala Insights | Epochs & Echoes World | Investor Central Medium | Puzzling Mysteries Medium | Science & Epochs Medium | Modern Hindutva

Top comments (0)