DEV Community

Cover image for Improving Code Quality in React with JavaScript Best Practices
Suraj Vishwakarma
Suraj Vishwakarma

Posted on • Edited on

Improving Code Quality in React with JavaScript Best Practices

Introduction

Writing code is easy but writing optimized, clean, and readable code takes more than applying logic. It takes some experience with the technology along with learning from other code and developers. One of the quotes from the Clean Code book by Robert Cecil Martin says

It is not enough for the code to work
-Robert C. Martin

The quote emphasizes writing optimized code along with the code that works.

React has been the most popular framework for building frameworks. Even there is a framework such as NextJS that uses React under the hood to build a better framework. So, knowing how to write better code in React can help in writing better code in many other frameworks.

So, today we are going to look into some code examples that show best practices that you can use to improve the JavaScript code quality in React.

Let’s get started.

1. Props Destructuring and Prop Types

Rather than using props and then using props.variableName, it is better to restructure the props to get the properties. It helps other developers to know which properties are available in the code. Destructing props helps in writing cleaner and readable code.

Along with destructuring the props, you should also validate the types of props using ‘prop-types’. You can set up the project in TypeScript for adding type-safety to React. TypeScript is popular for providing type safety to the application. If you do not want to use TypeScript for your whole project then use the ‘prop-types’ library to add type-safety to your props.

Here is the code example demonstrating props destructuring and types safety:

    import React, { useState, useEffect } from 'react';

    function MyComponent({ initialCount }) {
      const [count, setCount] = useState(initialCount);

      useEffect(() => {
        document.title = `You clicked ${count} times`;
      });

      return (
        <div>
          <p>You clicked {count} times</p>
          <button onClick={() => setCount(count + 1)}>
            Click me
          </button>
        </div>
      );
    }
Enter fullscreen mode Exit fullscreen mode

2. Optimizing Performance with useMemo and useCallback Hook

useMemo and useCallback are the two hooks that can help in optimizing the performance of the application in React.

useMemo

This is used to memorize an expensive(resource-expensive) computed value that doesn’t need to be recalculated when re-rendering. It is recalculated if any dependencies are required to calculate the value changes. This will save resources and improve the performance of the application.

Use Case of useMemo Hook:

    const MyComponent = ({ list }) => {
      const sortedList = useMemo(() => {
        return list.sort();
      }, [list]);

      return (
        <div>
          {sortedList.map(item => <div key={item}>{item}</div>)}
        </div>
      );
    };
Enter fullscreen mode Exit fullscreen mode

useCallback

This hook is the same as useMemo but for the callback function. It memorizes the callback function. This is useful when passing callbacks to optimized child components that rely on reference equality(same memory address rather than content) to prevent unnecessary renders.

Use Case of usecallback Hook:

    const MyComponent = ({ a, b }) => {
      const memoizedCallback = useCallback(() => {
        return a + b;
      }, [a, b]);

      return <ChildComponent onCalculate={memoizedCallback} />;
    };
Enter fullscreen mode Exit fullscreen mode

3. Custom Hook

Creating custom hooks in React can help in encapsulating and reusing logic across multiple components. They are specially designed to work within the React component lifecycle and have access to other React features like state and other hooks.

Here is an example that shows the fetching of Data using API. You can create a custom hook for fetching data use across the application.

    import { useState, useEffect } from 'react';

    function useFetchData(url) {
      const [data, setData] = useState(null);
      const [loading, setLoading] = useState(true);
      const [error, setError] = useState(null);

      useEffect(() => {
        fetch(url)
          .then(response => response.json())
          .then(data => {
            setData(data);
            setLoading(false);
          })
          .catch(error => {
            setError(error);
            setLoading(false);
          });
      }, [url]);

      return { data, loading, error };
    }
Enter fullscreen mode Exit fullscreen mode

This also shows why you can prefer a custom hook over any regular function. Here are those:

  • State and Effect Management: You can use the React logic and hooks like useState and useEffect for better state management and writing cleaner code.
  • Sharing Logic: Custom hooks provide an elegant way to share logic between components without resorting to rendering props or higher-order components. You can easily reuse complex logic across your project.

4. Using Error Boundary and Suspense

Use Error Boundary and Suspense together for a robust error handling and loading experience for your application. Let’s look into both of them.

Error Boundary

Error boundary is a React feature that helps in catching JavaScrpt errors in the component. Based on the error, it also renders a fallback UI instead of crashing the entire application. This can be used to manage errors in applications in a better way and also improve the user experience of the application.

Error boundary can be defined in a class and then used in functional components. Here is an example of this:

ErrorBoundary Class

    import React from 'react';

    class ErrorBoundary extends React.Component {
      state = { hasError: false, error: null };

      static getDerivedStateFromError(error) {
        return { hasError: true, error };
      }

      componentDidCatch(error, errorInfo) {
        console.error("Error caught by Error Boundary:", error, errorInfo);
      }

      render() {
        if (this.state.hasError) {
          return <h1>Something went wrong: {this.state.error.message}</h1>;
        }
        return this.props.children;
      }
    }

    export default ErrorBoundary;
Enter fullscreen mode Exit fullscreen mode

Functional Component

    import React from 'react';
    import ErrorBoundary from './ErrorBoundary'; 

    const MyComponent = () => {
      // Component logic here. For demonstration, we'll keep it simple.
      return <div>MyComponent content</div>;
    };

    const App = () => {
      return (
        <ErrorBoundary>
          <MyComponent />
        </ErrorBoundary>
      );
    };

    export default App;
Enter fullscreen mode Exit fullscreen mode

Suspense

Suspense is also a React feature that lets you “suspend” a component while waiting for some asynchronous operation. It can be used while fetching data or code splitting. It also renders a fallback UI while waiting for the content to load. It combines with React.lazy() to import the child component.

Here is an example showing the use case of Suspense.

    import React, { Suspense } from 'react';
    const LazyComponent = React.lazy(() => import('./LazyComponent'));

    function MyComponent() {
      return (
        <Suspense fallback={<div>Loading...</div>}>
          <LazyComponent />
        </Suspense>
      );
    }
Enter fullscreen mode Exit fullscreen mode

Combining Error Boundaries and Suspense

Both components can be combined to provide better error handling and improve the user experience. You can wrap a Suspense component in an error boundary to catch any errors that might occur during loading or in the lazy-loaded components.

    <ErrorBoundary>
      <Suspense fallback={<div>Loading...</div>}>
        <LazyComponent />
      </Suspense>
    </ErrorBoundary>
Enter fullscreen mode Exit fullscreen mode

5. Immutable Data Patterns

It is a method that allows the state to be immutable, you don’t modify the state of an array or object directly but instead return a new array or object that represents the new state. This makes the state changes predictable and transparent. We can also track the changes to the state.

React's re-render optimization techniques, like PureComponent and React.memo, rely on shallow comparison to detect changes. An immutable state ensures that changes to the state produce new objects, making these comparisons reliable and efficient.

Here is an example of Updating State in Array.

    const [items, setItems] = useState([{ id: 1, name: "Item 1" }]);

    // Adding an item (without mutating the original state)
    const addItem = newItem => {
      setItems([...items, newItem]);
    };

    // Removing an item (without mutating the original state)
    const removeItem = itemId => {
      setItems(items.filter(item => item.id !== itemId));
    };
Enter fullscreen mode Exit fullscreen mode

We can use this object as per the below example.

    const [user, setUser] = useState({ name: "Alice", age: 30 });

    // Updating a property (without mutating the original state)
    const updateUser = newName => {
      setUser({ ...user, name: newName });
    };

    // Removing a property (without mutating the original state)
    const removeProperty = propName => {
      setUser(prevUser => {
        const { [propName]: _, ...rest } = prevUser;
        return rest;
      });
    };
Enter fullscreen mode Exit fullscreen mode

Connect With Me

Let's connect and stay informed on all things tech, innovation, and beyond! 🚀

Conclusion

Embracing best practices in React goes beyond just writing functional code; it's about creating efficient, readable, and maintainable applications. Techniques like props destructuring enhance clarity while using useMemo and useCallback optimizes performance. Custom hooks demonstrate React's power in logic reuse and sharing across components. Additionally, integrating error boundaries and Suspense ensures robust error handling and a smooth user experience, particularly in scenarios of data fetching and component loading.

Adopting immutable data patterns is crucial for maintaining predictable state mutations, aligning with React's optimization strategies for component lifecycle and performance. In essence, these practices are not merely for meeting functional requirements but are fundamental in building resilient, performant, and scalable applications, reflecting the ethos of writing truly high-quality code in React.

I hope this article will help you in writing better code in React. Thanks for reading the article.

Top comments (1)

Collapse
 
treio profile image
Marek Šmerda

You should use the set function of useState like this:

const updateUser = (newName) => {
  setUser((user) => ({ ...user, name: newName }));
};
Enter fullscreen mode Exit fullscreen mode