DEV Community

Cover image for Part 4: Writing Clean and Efficient React Code- Best Practices and Optimization Techniques
Sathish Kumar N
Sathish Kumar N

Posted on • Updated on

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

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.

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

// ❌ 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={true} />
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!😊👩‍💻👨‍💻

Top comments (19)

Collapse
 
pengeszikra profile image
Peter Vivo

Instead of use prop types use typescript with FC

import {FC} from 'react';

interface Button {
  text:string;
  enabled:boolean;
}

const Button:FC<Button> = ({ text, enabled }) => (
  <button enabled>{text}</button>
);
Enter fullscreen mode Exit fullscreen mode
Collapse
 
sathishskdev profile image
Sathish Kumar N

Thank you for your suggestion!

If you are using JavaScript (without TypeScript), you can continue using prop-types to validate the props in your components.

If you are using TypeScript, you can benefit from the additional type-checking capabilities it offers by using the FC type to define your components' props.

Collapse
 
pengeszikra profile image
Peter Vivo

Does not have any reason to not use TS.

Collapse
 
merri profile image
Vesa Piittinen • Edited

But you don't need FC.

import type { ReactNode } from 'react';

interface Props {
  children: ReactNode;
  enabled: boolean;
}

export const Button = ({ children, enabled = true }: Props) => (
  <button aria-disabled={!enabled || undefined}>{children}</button>
);

// or

export function Button({ children, enabled = true }: Props) {
  return (
    <button aria-disabled={!enabled || undefined}>{children}</button>
  );
}

// or
import { forwardRef } from 'react';

export const Button = forwardRef<HTMLButtonElement, Props>(function Button(
  { children, enabled = true),
  ref
) {
  return (
    <button ref={ref} aria-disabled={!enabled || undefined}>{children}</button>
  );
});
Enter fullscreen mode Exit fullscreen mode
Collapse
 
leocaprile profile image
Matías Fuentes

FC is a unnecessary import, you can destructure the props as the examples below

Collapse
 
r4dic4l profile image
Oana Maria Teodorescu

There's a small mistake in no. 10: Shorthand for boolean props, should be multiSelect={true} replaced by multiSelect prop instead of multiSelect={false}.

Image description

Collapse
 
sathishskdev profile image
Sathish Kumar N • Edited

Thank you for bringing the issue!

I updated the snippet now.

Collapse
 
brense profile image
Rense Bakker

I think you're mistaken between named exports and named functions. If you write your components like this:

export default function MyNamedFunctionComponent(){
}
Enter fullscreen mode Exit fullscreen mode

Your editor has no trouble finding out what you mean when you try to use MyNamedFunctionComponent.

The same goes for variables like this:

const MyNamedConponent(){}
export default MyNamedConponent
Enter fullscreen mode Exit fullscreen mode

What you DONT want to do is:

// Code smell anonymous default export...
export default function(){}
Enter fullscreen mode Exit fullscreen mode

And infact recommended react eslint rules will warn you not to do that.

I'm also quite unsure if your tree shaking claim is true... If it is true it would only be for webpack 3 maybe. I'm definitely not seeing "use default exports" on this checklist: smashingmagazine.com/2021/05/tree-...

Collapse
 
sathishskdev profile image
Sathish Kumar N • Edited

I apologize for any confusion caused in my blog.

I am familiar with the concept of named and default exports. Please refer to the following code snippet to understand the difference between default and named exports:

// ✅ Use named export
export function MyNamedFunctionComponent() {
  // Component logic...
}

// ❌ Avoid default export
export default function MyComponent() {
  // Component logic...
}
Enter fullscreen mode Exit fullscreen mode

In the case of default exports, refactoring can be more challenging. Here's an example:

Default export:

// In Component
export default function MyComponent() {...}

// When importing in the first component
import component from "./MyComponent"

// When importing in another component
import myComponent from "./MyComponent"
Enter fullscreen mode Exit fullscreen mode

However, when using named exports, this confusion can be avoided.

Regarding the claim about tree shaking, it's important to note that tree shaking itself is not directly dependent on the use of default exports.

By utilizing named exports, you gain more control over the exported values. This allows unused values to be eliminated during the bundling process, resulting in a smaller bundle size.

Collapse
 
brense profile image
Info Comment hidden by post author - thread only accessible via permalink
Rense Bakker

Named exports are objects though. If you have one file, with several named exports, in the background, it just looks like a big object to the bundler and all that code will be included, even if you only use one of the named exports in your code. That is exactly why some libraries recommend you use their default exports and NOT their named exports, because the named exports are all exported from a single index.ts file.

If you really want to be religious about optimizing for tree shaking, it doesnt matter what kind of export you use, named or default. The only rule is: export one thing per file. In reality though, it doesnt matter that much. If you have some utils file with a bunch of named exports, it doesnt matter that they all get included in the bundle, because the overhead of some small functions is minimal.

 
sathishskdev profile image
Info Comment hidden by post author - thread only accessible via permalink
Sathish Kumar N • Edited

I agree your point!

In Tree shaking, the key is to import only the specific exports you need, whether they are named or default exports.

When we using default exports, we don't need to specify a specific name when importing that value into another file. You can give it any name you want when you import it.

 
brense profile image
Info Comment hidden by post author - thread only accessible via permalink
Rense Bakker

You are ChatGPT :P

 
sathishskdev profile image
Info Comment hidden by post author - thread only accessible via permalink
Sathish Kumar N

I didn't use ChatGPT for my comments!

Collapse
 
sney profile image
Snehil

Are you using chat gpt for your replies haha? Same tone of voice..

Thread Thread
 
sathishskdev profile image
Sathish Kumar N • Edited

Ha ha! I am using Quillbot to convert my sentence and improve it further.

Collapse
 
insidewhy profile image
insidewhy • Edited

I wonder, why do you write all your exports at the bottom?

  1. You have to type every exported declaration name twice, which is usually a lot more characters than typing export next to each exported declaration.
  2. Whether a declaration is exported or not is extremely important information, I shouldn't have to scroll to the bottom of the file before I find out whether something is exported or not.
Collapse
 
borobudur01 profile image
borobudur01

Excellents points. I agree with all of them.
About How you "Avoid huge component" I believe what you do with your components is just as bad if not worse tho. The modularity is too extreme and at the end, it's a nightmare to work with.

Collapse
 
nvrtmd profile image
yuza • Edited

Hello,

I thoroughly enjoyed reading your React Best Practices in 2023 series. I'm a developer from South Korea, and I'm interested in translating the four articles included in your React Best Practices in 2023 series into Korean. I would like to post them on my personal blog with proper attribution and links. Would that be possible? I believe it would make it more convenient for Korean developers to access your content. Looking forward to your response :) Have a great day!

Collapse
 
silverium profile image
Soldeplata Saketos

React.memo is less performant than just rendering the component again. Under the hood, the memo function has more operations than rendering a simple component

Some comments have been hidden by the post's author - find out more