Skip to content
Menu
Rohit Naik Kundaikar
  • Home
  • Contact
Rohit Naik Kundaikar

React Best Practices for Performance and Maintainability

Posted on January 25, 2024July 19, 2025 by Rohit Naik Kundaikar

Building efficient and scalable React applications requires more than just knowing the syntax. Adhering to certain best practices can significantly improve your app’s performance, maintainability, and readability. This post will cover some crucial techniques to optimize your React development workflow.

1. Function Recreation: Utilizing useCallback

In React, whenever a component re-renders, all functions declared within that component are recreated. While this might not be noticeable in small applications, it can adversely impact performance in larger, more complex apps, especially when these functions are passed down as props to child components.

To mitigate this, the useCallback hook is crucial. It allows you to memoize a function, preventing its recreation on every re-render unless its dependencies have changed.

Example:

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

function ParentComponent() {
  const [count, setCount] = useState(0);

  // Without useCallback, this function is recreated on every render
  // const handleClick = () => {
  //   setCount(prevCount => prevCount + 1);
  // };

  // With useCallback, this function is memoized.
  // It only recreates if `setCount` (which is stable) changes.
  const handleClick = useCallback(() => {
    setCount(prevCount => prevCount + 1);
  }, []); // Empty dependency array means it's created once

  return (
    <div>
      <p>Count: {count}</p>
      <ChildComponent onButtonClick={handleClick} />
    </div>
  );
}

// ChildComponent might be wrapped with React.memo to prevent unnecessary re-renders
const ChildComponent = React.memo(({ onButtonClick }) => {
  console.log('ChildComponent rendered');
  return (
    <button onClick={onButtonClick}>Increment Count</button>
  );
});

export default ParentComponent;

By wrapping handleClick with useCallback, we ensure that ChildComponent (especially if it’s memoized) doesn’t re-render unnecessarily just because onButtonClick is a new function reference.

2. Optimizing Sibling Re-renders

A common performance pitfall occurs when a state change in a parent component causes a re-render that doesn’t affect all its children. This leads to unnecessary re-rendering of unaffected sibling components.

There are two primary approaches to address this:

Option A: Make State-Dependent Child Components Impure or Move State Logic

This is often the simpler and less costly approach. If a child component only needs to re-render when its specific state or props change, you can either:

  • Move the state logic down: If a piece of state only affects a specific child, move that state and its logic directly into the child component. This isolates the re-render.
  • Ensure component purity: Design components to only re-render when their props actually change. React’s default behavior is often sufficient, but sometimes explicit React.memo is needed (see Option B).

Option B: Wrap Child Components with React.memo

The other approach is to wrap the child components with the React.memo higher-order component. This provides similar results by preventing re-renders if the props haven’t changed, but at the expense of extra computation for React to check for prop changes. Use React.memo judiciously, as the overhead of the prop comparison might outweigh the re-render cost for very simple components.

Example:

import React, { useState } from 'react';

// Component that should only re-render if its specific prop changes
const MemoizedChild = React.memo(({ value }) => {
  console.log('MemoizedChild rendered with value:', value);
  return <p>Memoized Value: {value}</p>;
});

// Another child that doesn't depend on the parent's specific state
const StaticChild = () => {
  console.log('StaticChild rendered');
  return <p>This is a static child.</p>;
};

function ParentWithState() {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('Alice');

  return (
    <div>
      <h2>Parent Component</h2>
      <button onClick={() => setCount(count + 1)}>Increment Count</button>
      <button onClick={() => setName('Bob')}>Change Name</button>

      <p>Parent Count: {count}</p>
      {/* MemoizedChild only re-renders when 'count' changes */}
      <MemoizedChild value={count} />
      {/* StaticChild will re-render if ParentWithState re-renders,
          unless its own props or internal state cause it to re-render.
          If it's truly static, consider moving it out or memoizing if it's complex. */}
      <StaticChild />
    </div>
  );
}

export default ParentWithState;

In this example, MemoizedChild will only re-render when count changes, not when name changes.

3. Single Responsibility Principle (SRP) for Components

Just like in general software design, the Single Responsibility Principle applies to React components. A component should have a single, well-defined responsibility. For instance, a component might be responsible for:

  • Rendering data: Displaying a list of items.
  • Handling user input: A form component.
  • Managing a specific piece of UI state: A toggle button.

Other operations, such as data fetching, complex business logic, or global state management, should be delegated to:

  • Custom Hooks: For reusable stateful logic.
  • Context/Redux/Zustand (Stores): For global state management.
  • Subcomponents: Breaking down complex UIs into smaller, manageable pieces.

This practice significantly improves code readability, testability, and reusability.

Example (Conceptual):

// Bad: Component doing too much
// class UserProfile extends React.Component {
//   // Fetches data, manages form state, renders UI
// }

// Good: Delegating responsibilities
import React, { useState, useEffect } from 'react';

// Custom Hook for data fetching (Single Responsibility)
const useUserData = (userId) => {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    const fetchUser = async () => {
      setLoading(true);
      // Simulate API call
      await new Promise(resolve => setTimeout(resolve, 500));
      setUser({ id: userId, name: `User ${userId}`, email: `user${userId}@example.com` });
      setLoading(false);
    };
    fetchUser();
  }, [userId]);

  return { user, loading };
};

// UserProfileDisplay component (Single Responsibility: rendering user data)
const UserProfileDisplay = ({ user, loading }) => {
  if (loading) return <p>Loading user data...</p>;
  if (!user) return <p>User not found.</p>;

  return (
    <div>
      <h3>User Profile</h3>
      <p>Name: {user.name}</p>
      <p>Email: {user.email}</p>
    </div>
  );
};

// Parent component orchestrating (can also be a container component)
function UserProfilePage({ userId }) {
  const { user, loading } = useUserData(userId); // Delegate data fetching

  return (
    <div>
      <h1>User Details</h1>
      <UserProfileDisplay user={user} loading={loading} /> {/* Delegate rendering */}
    </div>
  );
}

export default UserProfilePage;

4. React Fragments (<></>)

In React, it is a requirement that all adjacent JSX elements returned by a component be enclosed within a single parent element. Often, to meet this requirement, developers use a div element unnecessarily, introducing an extra element with no semantic purpose on the page, potentially affecting layout or increasing DOM depth.

This redundancy can be eliminated by opting for React Fragments, represented by <></> (short syntax) or <React.Fragment>. They act as dummy elements that group children without adding extra nodes to the DOM.

Example:

import React from 'react';

function ProductDetails() {
  return (
    // Bad: Unnecessary div wrapper
    // <div>
    //   <h2>Product Name</h2>
    //   <p>Description of the product.</p>
    //   <span>Price: $19.99</span>
    // </div>

    // Good: Using React Fragment
    <>
      <h2>Product Name</h2>
      <p>Description of the product.</p>
      <span>Price: $19.99</span>
    </>
  );
}

export default ProductDetails;

5. Lazy Loading Components

Lazy loading is the optimal method for loading modules on demand, significantly improving initial application load time. If the usage of a particular module or component depends on conditions, such as user interaction, access rights, or specific navigation paths, these modules need not be loaded during the initial page load.

React’s lazy function, combined with Suspense, facilitates this behavior. Components are loaded only when they are actually needed, reducing the initial bundle size.

Example:

import React, { lazy, Suspense, useState } from 'react';

// Lazy load a component
const LazyComponent = lazy(() => import('./LazyLoadedComponent'));

function App() {
  const [showLazy, setShowLazy] = useState(false);

  return (
    <div>
      <h1>My App</h1>
      <button onClick={() => setShowLazy(true)}>Load Lazy Component</button>

      {showLazy && (
        <Suspense fallback={<div>Loading...</div>}>
          <LazyComponent />
        </Suspense>
      )}
    </div>
  );
}

// In a separate file, e.g., LazyLoadedComponent.js
// const LazyLoadedComponent = () => {
//   return <h2>I am a lazily loaded component!</h2>;
// };
// export default LazyLoadedComponent;

export default App;

In this example, LazyComponent will only be fetched and rendered when the “Load Lazy Component” button is clicked. While it’s loading, the fallback content of Suspense is displayed.

Conclusion

Implementing these React best practices will lead to more performant, maintainable, and enjoyable development experiences. By focusing on function memoization, efficient re-renders, clear component responsibilities, optimized DOM structure, and on-demand loading, you’ll build robust applications that scale effectively.

React Best Practices: Part 1
Best Practice, ReactJS

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

Recent Posts

  • Mastering Clean Code: A Deep Dive into SOLID Principles
  • Building a Context-Aware Q&A System with LangChain.js and Web Scraping
  • TypeScript Best Practices for React
  • Vite Micro Frontend
  • React Best Practices for Performance and Maintainability

Recent Comments

    Archives

    • July 2025
    • August 2024
    • January 2024
    • September 2021
    • July 2021
    • June 2021
    • May 2021
    • April 2021
    • December 2020
    • November 2020
    • October 2020
    • September 2020

    Categories

    • Angular
    • API
    • Best Practice
    • Compiler
    • Context
    • DevOps
    • Docker
    • FAANG
    • Forms
    • Good Coding Habits
    • GraphQL
    • Java
    • Javascript
    • LangChain
    • LLM
    • Machine Learning
    • MobX
    • Python
    • ReactJS
    • Redux Toolkit
    • Spring Boot
    • Typescript
    • Uncategorized
    • Vite
    • Webpack
    ©2025 Rohit Naik Kundaikar | Powered by WordPress & Superb Themes