Part 4: Writing Clean and Efficient React Code- Best Practices and Optimization Techniques

Share this post:


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:

// 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} />;
  };
};

Enter fullscreen mode

Exit fullscreen mode

Usage:

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

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

const WrappedComponent = withErrorBoundary(Todo);
Enter fullscreen mode

Exit fullscreen mode




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,

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

// Todo

const Todo = () => {
 //... Component logic

return <div>
 //.. Other elements
   <TodoItem //.. />
</div>
}
Enter fullscreen mode

Exit fullscreen mode

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.

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

Exit fullscreen mode

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.

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

export default Todo;  
Enter fullscreen mode

Exit fullscreen mode

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

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

}

export { Todo };
Enter fullscreen mode

Exit fullscreen mode

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.




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.

// ❌ 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;
Enter fullscreen mode

Exit fullscreen mode

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.

// ✅ Use object destructuring
const { id, name = "Task", completed } = todo; 
Enter fullscreen mode

Exit fullscreen mode

  • 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.

// ❌ Avoid unnecessary wrapper div
const Todo = () => (
  <div>
    <h1>Title</h1>
    <ul>
      // ...
    </ul>
  </div>
);
Enter fullscreen mode

Exit fullscreen mode

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

// ✅ Use fragments
const Todo = () => (
  <>
    <h1>Title</h1>
    <ul>
      // ...
    </ul>
  </>
);
Enter fullscreen mode

Exit fullscreen mode




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.

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

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

Exit fullscreen mode

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.

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

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

updateTodo(todo);
Enter fullscreen mode

Exit fullscreen mode

  • 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.

// ❌
function sum(a, b) {
  return a + b;
}
Enter fullscreen mode

Exit fullscreen mode

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

// ✅ Use arrow function
const sum = (a, b) => a + b;
Enter fullscreen mode

Exit fullscreen mode

  • 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

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

Exit fullscreen mode

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

// ✅ 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 //...
}
Enter fullscreen mode

Exit fullscreen mode

  • 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

// ❌
<Dropdown multiSelect={false} />
Enter fullscreen mode

Exit fullscreen mode

// ✅ Use shorthand
<Dropdown multiSelect />
Enter fullscreen mode

Exit fullscreen mode

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




11. Avoid using indexes as key props

// ❌ Avoid index as key
const renderItem = (todo, index) => {
  const {name} = todo;
  return <li key={index}> {name} </>
}
Enter fullscreen mode

Exit fullscreen mode

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.

// ✅ Using unique and stable identifiers
const renderItem = (todo, index) => {
  const {id, name} = todo;
  return <li key={id}> {name} </>
}
Enter fullscreen mode

Exit fullscreen mode

  • Efficiently update and reorder components in lists.

  • Reducing potential rendering issues.

  • Avoids in-correct component update.




12. Use implicit return in small functions

// ❌ Avoid using explicit returns 
const square = value => {
  return value * value;
}
Enter fullscreen mode

Exit fullscreen mode

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.

// ✅ Use implicit return
const square = value => value * value;
Enter fullscreen mode

Exit fullscreen mode

  • 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

// ❌ Bad Code
const Button = ({ text, enabled }) => 
      <button enabled>{text}</button>;
Enter fullscreen mode

Exit fullscreen mode

Above code can lead to passing incorrect prop types, which may result in runtime errors or unexpected behavior.

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

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

Button.propTypes = {
  enabled: PropTypes.bool
  text: PropTypes.string.isRequired,
};
Enter fullscreen mode

Exit fullscreen mode

  • 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

// ❌ Bad Code
const title = (seriesName) => 
      "Welcome to " + seriesName + "!";
Enter fullscreen mode

Exit fullscreen mode

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

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

Exit fullscreen mode

  • 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:

// ❌ 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;  
Enter fullscreen mode

Exit fullscreen mode

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.


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!😊👩‍💻👨‍💻



Source link