In today's adventure I will take you to the Other Side of code development. As ONE, we'll explore the art of code refactoring and see how it can revolutionize your codebase. Sounds marvelous!!! After going through this adventure, you'll learn how to transform your code from Good to Great or Great to Next level. You're a developer and it's your responsibility to take your application to new heights of performance, scalability and maintainability.
As software projects grow, it's common for the codebase to become complex & difficult to manage. At this point, code refactoring comes into light from the darkness. It's the process of improving existing code by restructuring it in a more efficient and effective way without changing its behavior.
Refactoring does not add any new features or functionalities to the software or product. It can improve the quality, readability and maintainability of your codebase. Which makes it easier to work with in the long run.
Why Refactor Your Code?
Improved Code Quality:
Refactoring improves code quality by reducing code complexity, removing duplication and improving the overall design of the code. This ends in code that is easier to read and understand, reducing the risk of introducing errors or bugs.
Increased Maintainability:
By reducing code complexity and improving the design of your code, refactoring makes it easier to maintain and update your code in the long term. This can save time and effort for developers working on the project and reduce the risk of introducing bugs when making changes.
Better Performance:
Refactoring can help improve the performance of your code by optimizing algorithms, removing unnecessary code and reducing resource usage. This can lead to faster execution times and improved efficiency.
Easier Collaboration:
By improving code quality and reducing complexity, refactoring can make it easier for developers to collaborate on a project. This can improve team productivity and reduce the risk of introducing errors or bugs when working on shared code.
By moving forward on our adventure, it's time to perform the code refactoring and break on through to the Other Side.
Code Formatting
It's important to consider code formatting when working on a team or maintaining a codebase. Different developers may have varying preferences for indentation, line breaks and quotation marks. That ends in an inconsistent code style. Inconsistent code style can make the code look cluttered and challenging to read. So it's crucial to maintain a consistent code style across the project.
It's beneficial to use refactoring tools such as Prettier. In case you're not familiar with Prettier, let's break it.. Prettier is a popular and user friendly tool that automates code formatting. Once added to the project, it will take care of formatting the code according to pre set style settings. Also you can customize the formatting rules to your preference by creating a .prettierrc
file with below configs:
{
"arrowParens": "always",
"bracketSameLine": false,
"bracketSpacing": true,
"embeddedLanguageFormatting": "auto",
"htmlWhitespaceSensitivity": "css",
"insertPragma": false,
"jsxSingleQuote": false,
"printWidth": 80,
"proseWrap": "preserve",
"quoteProps": "as-needed",
"requirePragma": false,
"semi": true,
"singleAttributePerLine": false,
"singleQuote": false,
"tabWidth": 2,
"trailingComma": "es5",
"useTabs": false,
"vueIndentScriptAndStyle": false
}
Class Component Without State or Lifecycle Methods in React
Original code:
import React, { Component } from "react";
class Adventure extends Component {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
console.log("Migration happened");
}
render() {
return (
<div>
<p>Break on Through To the Other Side</p>
<button onClick={this.handleClick}>Migrate me</button>
</div>
);
}
}
Refactored code:
import React from "react";
function Adventure() {
const handleClick = () => {
console.log("Migration happened");
};
return (
<div>
<p>Break on Through To the Other Side</p>
<button onClick={handleClick}>Migrate me</button>
</div>
);
}
If the component needs state or lifecycle methods then use class component otherwise use function component.
From React 16.8 with the addition of Hooks, you could use state, lifecycle methods and other features that were only available in class component right in your function component. So, it is always recommended to use Function components, unless you need a React functionality whose Function component equivalent is not present yet, like Error Boundaries.
Avoid extra wrapping <div>
in React
Original code:
import React from "react";
function Adventure() {
return (
<div>
<p>Break on Through To the Other Side</p>
<button>Migrate me</button>
</div>
);
}
Refactored code:
import React, { Fragment } from "react";
function Adventure() {
return (
<Fragment>{/* or use a shorter syntax <> </> */}
<p>Break on Through To the Other Side</p>
<button>Migrate me</button>
</Fragment>
);
}
It's a common pattern in React which is used for a component to return multiple elements. Fragments let you group a list of children without adding extra nodes to the DOM.
Why fragments are better than container divs
?
- Fragments are a bit faster and use less memory by not creating an extra DOM node. This only has a real benefit on very large and deep trees.
- Some CSS mechanisms like Flexbox and CSS Grid have a special parent-child relationships and adding
divs
in the middle makes it hard to keep the desired layout.
Naming Conventions
Naming is a important part of writing clean and maintainable code and it's required to take the time to choose descriptive and meaningful names for your functions and variables. When naming functions and variables, it's important to choose names that are self explanatory and convey the purpose and function of the code.
For React, a component name should always be in a Pascal case like UserDashboard
, Dashboard
etc.. Using Pascal case for components differentiate it from default JSX element tags.
Methods/functions defined inside components should be in Camel case like getUserData()
, showUserData()
etc.
For globally used Constant fields in the application, try to use capital letters only. As an instance,
const PI = 3.14;
Remove Unnecessary Comments from the Code
Original code:
import React, { Fragment } from "react";
function Adventure() {
console.log("test");
return (
<Fragment>
{/* <h1>Adventure</h1> */}
<p>Break on Through To the Other Side</p>
<button>Migrate me</button>
</Fragment>
);
}
Refactored code:
import React from "react";
function Adventure() {
return (
<>
<p>Break on Through To the Other Side</p>
<button>Migrate me</button>
</>
);
}
Add comments only where it's required so that you do not get confused while changing code at a later time.
Also don't forget to remove statements like console.log
, debugger, unused commented code.
Destructuring Props in React
Original code:
function Adventure(props) {
return (
<>
<h1>Hello, {props.username}</h1>
<button>Migrate me</button>
</>
);
}
Refactored code:
function Adventure({ username }) {
return (
<>
<h1>Hello, {username}</h1>
<button>Migrate me</button>
</>
);
}
Destructuring was introduced in ES6. This type of feature in the JavaScript function allows you to easily extract the form data and assign your variables from the object or array. Also, destructuring props make code cleaner and easier to read.
Respecting the Import Order
Original code:
import { formatCurrency, toNumber } from "@/utils";
import { useDrag } from "react-dnd";
import "./styleB.css";
import { useFormik } from "formik";
import React, { useEffect, useState } from "react";
import Button from "@/components/Button";
import TextField from "@/components/TextField";
import PropTypes from 'prop-types';
Refactored code:
import React, { useEffect, useState } from "react";
import PropTypes from 'prop-types';
import { useDrag } from "react-dnd";
import { useFormik } from "formik";
import { formatCurrency, toNumber } from "@/utils";
import Button from "@/components/Button";
import TextField from "@/components/TextField";
import "./styleB.css";
Organizing imports is a required part of writing clean and maintainable React code.
Import orders:
- React import
- Library imports (Alphabetical order)
- Absolute imports from the project (Alphabetical order)
- Relative imports (Alphabetical order)
- Import * as
- Import ‘./.
Each kind should be separated by an empty line. This makes your imports clean and easy to understand for all the components, 3rd-party libraries and etc.
Split your code into multiple smaller functions. Each with a single responsibility.
One of the key principles of writing clean and maintainable code is to keep functions and components small and focused. This means splitting your code into multiple smaller functions, each with a single responsibility.
When a function or component has too many responsibilities, it becomes harder to read, understand and maintain. By breaking it down into smaller, more focused functions, you can improve the readability and maintainability of your code.
Don't Repeat Yourself
Don't Repeat Yourself (DRY) is a fundamental principle of software development. Which encourages developers to avoid duplicating code or logic throughout their codebase. The DRY principle is a required aspect of writing maintainable and scalable code and it applies to both coding standards and code refactoring.
In terms of coding standards, the DRY principle suggests that you should avoid duplicating code or logic in your codebase. This means that you should strive to write reusable code that can be used in multiple places in your application. As an instance, instead of writing the same validation logic for every input field in your application, you could write a reusable function that performs the validation and use it in each input field.
It's Highly Recommended to Avoid Arrow Functions in Render
One way to improve the performance of your components is to avoid using arrow functions in the render() method.
What's the problem with below instance?
Every time a component is rendered, a new instance of the function is created. Actually, it's not a big deal in case the component is rendered one or two times. But in other cases, it can affect performance. Arrow functions in the render() method can cause unnecessary re renders of your components.
Original code:
function Adventure() {
const [message, setMessage] = useState("Migration not started");
return (
<>
<h1>Mesage: {message}</h1>
<button onClick={() => setMessage("Migration happened")}>
Migrate me
</button>
</>
);
}
So if you care about performance, declare the function before using it in render as below instance:
Refactored code:
function Adventure() {
const [message, setMessage] = useState("Migration not started");
const onMessage = () => {
setMessage("Migration happened");
};
return (
<>
<h1>Mesage: {message}</h1>
<button onClick={onMessage}>Migrate me</button>
</>
);
}
Instead of using arrow functions in the render()
method, you can declare methods as instance methods outside of the render()
method. This ensures that the method is only created once and is not recreated on each render.
Decrease React Bundle Size
When you're using third-party libraries in your React app, it's very important to keep the bundle size as small as possible. A large bundle size can negatively impact the performance of your application, especially on slower internet connections.
One way to reduce the bundle size is to only load the parts of the library that you actually need. Instead of importing the entire library, you can import only the specific method that you need using a technique called tree shaking. This can significantly reduce the size of your bundle.
Let's say you're using the popular library Lodash and you only need the pick()
method to pick certain properties from an object. Instead of importing the entire Lodash library, you can import only the pick()
method as below:
import pick from 'lodash/pick';
const pickedUserProps = pick(userProps, ['name', 'email']);
instead of
import lodash form 'lodash';
const pickedUserProps = lodash.pick(userProps, ['name', 'email']);
Use useMemo
to cache expensive calculations
When building a React app, it's common to perform calculations or generate data that can be computationally expensive. This can negatively impact the performance of your app, especially if these calculations are performed frequently or in large amounts.
To kill the performance issues of your app, you can use the useMemo hook in React to cache expensive calculations. useMemo
is a built in React hook that memoizes the result of a function, so that it is only recomputed when its dependencies have changed.
Let's say you have a component that generates a list of items based on some data that is fetched from an API. The list is generated using a function that performs some expensive calculations:
function generateList(data) {
// Perform some expensive calculations here
// ...
return list;
}
function Adventure() {
const [data, setData] = useState([]);
useEffect(() => {
// Fetch data from API
// ...
setData(data);
}, []);
const list = generateList(data);
return (
<ul>
{list.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
}
With the above instance, the generateList
function is called every time the component re-renders, even if the data hasn't changed. This can result in unnecessary computations and slow down the performance of your application.
To optimize this, you can use the useMemo
hook to cache the result of generateList
:
function Adventure() {
const [data, setData] = useState([]);
useEffect(() => {
// Fetch data from API
// ...
setData(data);
}, []);
const list = useMemo(() => generateList(data), [data]);
// ...
}
Remove too many if-else conditions
If-else statements are a common feature in most programming languages and they are often used to control the flow of code execution based on certain conditions. Though, too many if-else statements can make your code difficult to read, maintain and debug..
One approach to removing too many if-else conditions is to use the "switch" statement. The switch statement is similar to an if-else statement, but it is designed to handle multiple cases in a more concise and efficient way. Let's see an instance:
function checkGrade(grade) {
if (grade >= 90) {
return "A";
} else if (grade >= 80) {
return "B";
} else if (grade >= 70) {
return "C";
} else if (grade >= 60) {
return "D";
} else {
return "F";
}
}
In above instance, we are using an if-else statement to check the grade of a student and return a letter grade. This code can be refactored using the switch statement:
function checkGrade(grade) {
switch (true) {
case (grade >= 90):
return "A";
case (grade >= 80):
return "B";
case (grade >= 70):
return "C";
case (grade >= 60):
return "D";
default:
return "F";
}
}
As you can see, the switch statement is more concise and easier to read than the if-else statement. It also allows us to handle multiple cases in a more efficient way.
Another approach to removing too many if-else conditions is to use object literals. Object literals are a way of defining key and value pairs in JavaScript.
To show you what I mean:
function getAnimalSound(animal) {
if (animal === "dog") {
return "woof";
} else if (animal === "cat") {
return "meow";
} else if (animal === "cow") {
return "moo";
} else {
return "unknown";
}
}
In above instance, we are using an if-else statement to return the sound of an animal. This code can be refactored using object literals:
function getAnimalSound(animal) {
const animalSounds = {
dog: "woof",
cat: "meow",
cow: "moo"
}
return animalSounds[animal] || "unknown";
}
As you can see, the object literals approach is more concise and easier to read than the if-else and switch statement. It also allows us to define key and value pairs in a more flexible way..
End of an adventure
Code refactoring is all about breaking through the limitations of your current code and pushing towards a better, more efficient and effective version. It can be a daunting task, but with the right mindset, tools and best practices, you can take your code to the Other Side.
Code refactoring is not just about improving the quality of your code, but also about improving the overall user experience of your app. By putting in the effort to refactor your code, you can create a more responsive, intuitive and engaging app that users will love.
So, don't be afraid to break through to the other side. Hug the process of code refactoring and watch as your app rises to new heights..
Motivation
🍀Support
Please consider following and supporting us by subscribing to our channel. Your support is greatly appreciated and will help us continue creating content for you to enjoy. Thank you in advance for your support!