Part 4: Writing Clean and Efficient React Code- Best Practices and Optimization Techniques
Tags: react, bestpractices, javascript, cleancode
Published: 2023-06-18 06:18:39

Welcome to Part 4 of our series on "**React best practices in 2023**"! In this part, we will explore various techniques and strategies to write clean and efficient code in your React applications. By following these best practices, you can improve the **maintainability**, **performance**, and **readability** of your codebase.

Let's dive in and learn how to write clean and efficient React code that not only works well but is also easier to understand, maintain, and scale.

## 1. Implement error boundaries to handle component errors gracefully

Wrap your components or specific sections of your application with error boundaries to **catch and handle errors** in a controlled manner.

This prevents the **entire application from crashing** and provides a **fallback UI or error message**, improving the user experience and making it easier to debug issues.

**Higher-Order Component (HOC) - withErrorBoundary:**

```js
// HOC for error boundary
const withErrorBoundary = (WrappedComponent) => {
  return (props) => {
    const [hasError, setHasError] = useState(false);
    const [errorInfo, setErrorInfo] = useState(null);

    useEffect(() => {
      const handleComponentError = (error, errorInfo) => {
        setHasError(true);
        setErrorInfo(errorInfo);
        // You can also log the error to an error reporting service here
      };

      window.addEventListener('error', handleComponentError);

      return () => {
        window.removeEventListener('error', handleComponentError);
      };
    }, []);

    if (hasError) {
      // You can customize the fallback UI or error message here
      return <div>Something went wrong. Please try again later.</div>;
    }

    return <WrappedComponent {...props} />;
  };
};

```

**Usage:**

```js
// HOC for error boundary
import withErrorBoundary from './withErrorBoundary';

const Todo = () => {
  // Component logic and rendering
}

const WrappedComponent = withErrorBoundary(Todo);
```

---

## 2. Use React.memo for functional components

React.memo is a higher-order component that memoizes the result of a functional component, **preventing unnecessary re-renders**. 

By wrapping your functional components with React.memo, you can optimize performance by **skipping re-renders when the component's props have not changed**.

Here is an example,

```js
// ❌ 
const TodoItem = ({text}) => {
  return <div> {text} </div>
} 

// Todo

const Todo = () => {
 //... Component logic
 
return <div>
 //.. Other elements
   <TodoItem //.. />
</div>
}
```

In this example, we have a functional component called TodoItem that receives a name prop and renders a todo text. 

By default, the component will re-render whenever **Todo parent component** re-render.

To optimize performance, we can wrap **TodoItem** with React.memo, creating a memoized version of the component. This memoized component will **only re-render if its props have changed**.

```js
// ✅ Memoized version of TodoItem using React.memo
const TodoItem = React.memo(({text}) => {
  return <div> {text} </div>
}) 
```

By using React.memo, we can prevent unnecessary re-renders and optimize the performance of functional components. 

However, it's important to note that React.memo performs a **shallow comparison of props**. If your component receives complex data structures as props, ensure that you handle prop updates appropriately for accurate memoization.

---

## 3. Use Linting for Code Quality

Utilizing a linter tool, such as ESLint, can greatly improve code quality and consistency in your React projects. 

By using a linter, you can:

- Ensure consistent code style

- Catch errors and problematic patterns

- Improve code readability and maintainability

- Enforce coding standards and conventions

---

## 4. Avoid default export

The problem with default exports is that it can make it harder to understand which components are being imported and used in other files. It also limits the flexibility of imports, as default exports can only have a **single default export** per file.

```js
// ❌ Avoid default export
const Todo = () => {
  // component logic...
};

export default Todo;  
```

Instead, it's recommended to use named exports in React:

```js
// ✅ Use named export
const Todo = () => {

}

export { Todo };
```

Using named exports provides better **clarity** when importing components, making the codebase more organized and easier to navigate. 

- Named imports work well with **tree shaking**.

> Tree shaking is a term commonly used within a JavaScript context to describe the **removal of dead code**. It relies on the import and export statements to detect if code modules are exported and imported for use between JavaScript files.

- **Refactoring** becomes easier.

-  Easier to identify and **understand the dependencies** of a module.

---

## 5. Use object destructuring

When we use **direct property access** using **dot notation** for accessing individual properties of an object, will work fine for simple cases.

```js
// ❌ Avoid direct property access using dot notation
const todo = {
   id: 1,
   name: "Morning Task",
   completed: false
}

const id = todo.id;
const name = todo.name;
const completed = todo.completed;
``` 

This approach can work fine for simple cases, but it can become **difficult** and **repetitive** when dealing with larger objects or when only a subset of properties is needed.

**Object destructuring**, on the other hand, provides a more concise and elegant way to extract object properties. It allows you to destructure an object in a single line of code and assign multiple properties to variables using a syntax similar to object literal notation.

```js
// ✅ Use object destructuring
const { id, name = "Task", completed } = todo; 
```

- It reduces the need for **repetitive** object property access.

- Supports the assignment of **default values**.

- Allows variable **renaming** and **aliasing**.

---

## 6. Use fragments

Fragments allow for cleaner code by avoiding unnecessary wrapper divs when rendering multiple elements.

```js
// ❌ Avoid unnecessary wrapper div
const Todo = () => (
  <div>
    <h1>Title</h1>
    <ul>
      // ...
    </ul>
  </div>
);
``` 

In the above code, Unnecessary wrapper div can add unnecessary complexity to the DOM structure, potentially impacting the accessibility of your web page.

```js
// ✅ Use fragments
const Todo = () => (
  <>
    <h1>Title</h1>
    <ul>
      // ...
    </ul>
  </>
);
```
---

## 7. Prefer passing objects instead of multiple props

when we use multiple arguments or props are used to pass user-related information to component or function, it can be challenging to remember the **order and purpose** of each argument, especially when the number of arguments grows.

```js
// ❌ Avoid passing multiple arguments
const updateTodo = (id, name, completed) => {
 //...
}

// ❌ Avoid passing multiple props
const TodoItem = (id, name, completed) => {
  return(
    //...
  )
}
```

When the number of arguments increases, it becomes more challenging to maintain and refactor the code. There is an increased chance of making mistakes, such as **omitting an argument** or **providing incorrect values**. 

```js
// ✅ Use object arguments
const updateTodo = (todo) => {
 //...
}

const todo = {
   id: 1,
   name: "Morning Task",
   completed: false
}

updateTodo(todo);
```

- Function becomes more **self-descriptive** and **easier to understand**.

- Reducing the chances of errors caused by **incorrect argument order**.

- Easy to **add** or **modify** properties without changing the function signature.

- Simplify the process of **debugging** or **testing** functions to passing an object as an argument.

---

## 8. Use arrow functions

Arrow functions provide a more concise syntax and lexical scoping, eliminating the need for explicit `this` binding and improving code readability.

```js
// ❌
function sum(a, b) {
  return a + b;
}
```

Above code can result in verbose code and potentially lead to misunderstandings regarding the context and binding of `this`.

```js
// ✅ Use arrow function
const sum = (a, b) => a + b;
```
- Arrow functions make the code more **compact** and **expressive**.

- It **automatically bind the context**, reducing the chances of `this` related bugs.

- It improves code **maintainability**. 

---

## 9. Use enums instead of numbers or strings

```js
// ❌ Avoid Using numbers or strings
switch(status) {
  case 1:
    return //...
  case 2:
    return //...
  case 3:
    return //...
}
```

Above code that is harder to understand and maintain, as the meaning of numbers may not be immediately clear.

```js
// ✅ Use Enums
const Status = {
  NOT_STARTED: 1,
  IN_PROGRESS: 2,
  COMPLETED: 3
}

const { NOT_STARTED, IN_PROGRESS COMPLETED } = Status;

switch(status) {
  case NOT_STARTED:
    return //...
  case IN_PROGRESS:
    return //...
  case COMPLETED:
    return //...
}
```

- Enums have **meaningful** and **self-descriptive** values.

- Improve **code readability**.

- Reducing the chances of **typos** or **incorrect values**.

- Better **type checking**, **editor autocompletion**, and **documentation**.

---

## 10. Use shorthand for boolean props

```js
// ❌
<Dropdown multiSelect={true} />
```

```js
// ✅ Use shorthand
<Dropdown multiSelect />
```

Shorthand syntax for boolean props improves code **readability** by reducing unnecessary verbosity and making the intention clear.

---

## 11. Avoid using indexes as key props

```js
// ❌ Avoid index as key
const renderItem = (todo, index) => {
  const {name} = todo;
  return <li key={index}> {name} </>
}
```
Using indexes as key props can lead to **incorrect rendering ** especially when adding, removing, or reordering list items.

It can result in **poor performance** and **incorrect component updates**.

```js
// ✅ Using unique and stable identifiers
const renderItem = (todo, index) => {
  const {id, name} = todo;
  return <li key={id}> {name} </>
}
```
- Efficiently update and reorder components in lists.

- Reducing potential rendering issues.

- Avoids in-correct component update.

---

## 12. Use implicit return in small functions

```js
// ❌ Avoid using explicit returns 
const square = value => {
  return value * value;
}
```

When we use explicit return can make small function definitions **unnecessarily longer** and harder to read.

It may result in more cluttered code due to additional **curly braces** and **explicit return statements**.

```js
// ✅ Use implicit return
const square = value => value * value;
```

- Implicit return **reduces code verbosity**.

- Improves code **readability**.

- It can enhance code maintainability by focusing on the **main logic** rather than return statements.

---

## 13. Use PropTypes for type checking

```js 
// ❌ Bad Code
const Button = ({ text, enabled }) => 
      <button enabled>{text}</button>;
```
Above code can lead to passing **incorrect** prop types, which may result in **runtime errors** or **unexpected behavior**.

```js
// ✅ Use PropTypes
import PropTypes from 'prop-types';

const Button = ({ enabled, text }) => 
      <button enabled> {text} </button>;

Button.propTypes = {
  enabled: PropTypes.bool
  text: PropTypes.string.isRequired,
};
```
- It helps catch error on **compile time**.

- It provides **better understanding** and expected type of the component.

- PropTypes act as a **documentation** for other developers working with the component.

---

## 14. Prefer using template literals

```js
// ❌ Bad Code
const title = (seriesName) => 
      "Welcome to " + seriesName + "!";
```

Above code can result in **verbose** code and make string interpolation or concatenation more difficult.

```js
// ✅ Use template literals
const title = (seriesName) => 
      `Welcome to ${seriesName}!`;
```

- It simplify **string manipulation** by allowing variable interpolation within the string.

- It makes code more **expressive** and **easier to read**.

- It support multi-line strings without additional workarounds. 

- Improving code formatting.

---

## 15. Avoid huge component

Avoiding huge components in React is crucial for maintaining clean, modular, and maintainable code. 

Large components tend to be more complex, harder to understand, and prone to issues. Let's explore an example to illustrate this concept:

```js
// ❌ Avoid huge component
const Todo = () => {
  // State Management
  const [text, setText] = useState("");
  const [todos, setTodos] = useState([])
  //... More states

  // Event Handlers
  const onChangeInput = () => //...
  // Other event handlers

  return (
   <div>
      <input //.. />
      <input //.. />   
      <button //.. />
      <list //... >
        <list-item //..>
      </list/>
   </div>
  )
};

export default Todo;  
```
In the above example, we have a component called `Todo`, which contains **multiple state variables** and **event handlers** and **elements**. 

As the component grows, it becomes **harder to manage**, **debug** and **understand**.

You can check below blog for to address this, it's recommended to break down such huge components into **smaller**, **reusable**, and **focused components**.

{% embed https://dev.to/sathishskdev/part-3-component-structure-building-reusable-and-maintainable-components-in-react-54n6 %}

---

Stay tuned for an upcoming blog dedicated to **optimization techniques in React**. We'll explore additional strategies to enhance performance and efficiency in your components. Don't miss out on these valuable insights!

Happy coding!😊👩‍💻👨‍💻