DEV Community

Ansh Sheladiya
Ansh Sheladiya

Posted on

React.js Best Practices: Writing Clean, Scalable, and Maintainable Components

React makes it easy to build interactive user interfaces, but writing React code that remains maintainable as an application grows requires more than knowing hooks and JSX. Small decisions about component structure, state management, rendering, and side effects can significantly affect long-term development speed.

In production applications, I focus on keeping components predictable, separating responsibilities, and making data flow easy to understand. These practices help teams debug issues faster, avoid unnecessary complexity, and ship features without breaking existing functionality.

Let's explore practical React.js best practices using a runnable JavaScript example that demonstrates component separation, immutable state updates, derived values, validation, and clean event handling.

10 React.js Best Practices for Production Applications

1. Keep Components Small and Focused

A component should have a clear responsibility. Instead of building a single component that handles data fetching, validation, filtering, and rendering, split the interface into smaller components that are easier to test and reuse. Extract repeated UI patterns into shared components when doing so improves clarity.

2. Use State Only for Values That Change

React state should represent information that changes over time and affects rendering. Avoid storing values that can be calculated from existing props or state because duplicated state can become inconsistent. For example, calculate filtered products during rendering instead of maintaining a separate state variable for the filtered list.

3. Never Mutate State Directly

React works best when state updates create new objects or arrays rather than modifying existing values. Use methods such as map, filter, and the spread operator to update collections immutably. For complex nested state, consider a reducer or a dedicated state-management solution.

4. Use Effects for External Synchronization

The useEffect hook is intended for synchronizing React with external systems, such as browser APIs, subscriptions, timers, or network connections. Avoid using effects to calculate values that can be derived during rendering. Always clean up subscriptions and timers when the component unmounts or the effect reruns.

5. Handle Lists and Keys Correctly

When rendering collections, provide a stable and unique key for every item. Database IDs are generally preferable to array indexes because indexes can produce incorrect reconciliation when items are reordered, inserted, or removed. Avoid generating random keys during rendering because they force React to recreate elements unnecessarily.

6. Keep Event Handlers Predictable

Keep event handlers focused on a single action, such as adding a product or removing an item. Validate user input at the boundary and provide clear feedback when an operation cannot proceed. Functional state updates are particularly useful when the next state depends on the previous state.

7. Optimize Only When Measurement Justifies It

React provides tools such as memo, useMemo, and useCallback, but they should not be added automatically to every component. Start by writing straightforward code, profile the application, and optimize demonstrated bottlenecks. Premature memoization can increase complexity without producing a meaningful performance improvement.

8. Separate Business Logic from UI

Keep domain-specific calculations separate from rendering wherever practical. Small utility functions are easier to test independently and can be reused by different components. For larger applications, custom hooks can encapsulate reusable stateful behavior without coupling it to a particular visual layout.

9. Make Loading, Empty, and Error States Explicit

A production interface should account for more than its successful state. Display meaningful messages when data is unavailable, an operation fails, or a collection is empty. These states make the application easier to use and reduce confusion during unexpected situations.

10. Treat Accessibility and Security as Core Requirements

Use semantic HTML elements, associate labels with inputs, preserve keyboard accessibility, and communicate validation errors clearly. Never assume client-side validation provides security; validate and authorize sensitive operations on the server. Avoid rendering untrusted HTML directly, and keep secrets out of frontend bundles.

Practical Example: A Product Inventory Component

The following example uses React with modern JavaScript to implement a small inventory interface. It demonstrates component separation, controlled inputs, immutable updates, derived filtering, stable keys, and clear user feedback without requiring an additional state-management library.

import React, { useState } from "react";
import { createRoot } from "react-dom/client";

// Keep example data outside the component so it is not recreated on every render.
const initialProducts = [
  { id: 1, name: "Mechanical Keyboard", price: 2499, stock: 12 },
  { id: 2, name: "Wireless Mouse", price: 999, stock: 25 },
  { id: 3, name: "USB-C Hub", price: 1499, stock: 0 },
];

// Extract reusable UI into a focused component.
function ProductCard({ product, onRemove }) {
  console.log(`Rendering product: ${product.name}`);

  return (
    <article className="product-card">
      <h3>{product.name}</h3>
      <p>Price: ₹{product.price.toLocaleString("en-IN")}</p>
      <p>Stock: {product.stock}</p>
      <button onClick={() => onRemove(product.id)}>
        Remove product
      </button>
    </article>
  );
}

function ProductInventory() {
  // State contains the source of truth for the inventory.
  const [products, setProducts] = useState(initialProducts);
  const [search, setSearch] = useState("");
  const [productName, setProductName] = useState("");
  const [productPrice, setProductPrice] = useState("");
  const [message, setMessage] = useState("");

  // Derive filtered products instead of duplicating them in state.
  const normalizedSearch = search.trim().toLowerCase();
  const filteredProducts = products.filter((product) =>
    product.name.toLowerCase().includes(normalizedSearch)
  );

  // Derive the total from current state to avoid stale totals.
  const totalProducts = products.length;
  const totalInventoryValue = products.reduce(
    (total, product) => total + product.price * product.stock,
    0
  );

  function handleAddProduct(event) {
    event.preventDefault();
    console.log("Step 1: Validating product input.");

    const name = productName.trim();
    const price = Number(productPrice);

    // Validate values before updating application state.
    if (!name || !Number.isFinite(price) || price <= 0) {
      setMessage("Enter a product name and a valid positive price.");
      console.log("Step 2: Validation failed.");
      return;
    }

    console.log("Step 2: Creating the new product.");
    const newProduct = {
      id: crypto.randomUUID(),
      name,
      price,
      stock: 1,
    };

    // Create a new array instead of mutating the existing state.
    setProducts((currentProducts) => [
      ...currentProducts,
      newProduct,
    ]);

    console.log("Step 3: Product added successfully.");
    setProductName("");
    setProductPrice("");
    setMessage(`${name} was added to the inventory.`);
  }

  function handleRemoveProduct(productId) {
    console.log(`Removing product: ${productId}`);

    // filter returns a new array and preserves the original state.
    setProducts((currentProducts) =>
      currentProducts.filter((product) => product.id !== productId)
    );
    setMessage("Product removal requested.");
  }

  return (
    <main>
      <h1>Product Inventory</h1>
      <p>Total products: {totalProducts}</p>
      <p>
        Inventory value: ₹
        {totalInventoryValue.toLocaleString("en-IN")}
      </p>

      <form onSubmit={handleAddProduct}>
        <label htmlFor="product-name">Product name</label>
        <input
          id="product-name"
          value={productName}
          onChange={(event) => setProductName(event.target.value)}
          placeholder="Enter product name"
          required
        />

        <label htmlFor="product-price">Price in rupees</label>
        <input
          id="product-price"
          type="number"
          min="0.01"
          step="0.01"
          value={productPrice}
          onChange={(event) => setProductPrice(event.target.value)}
          placeholder="Enter price"
          required
        />

        <button type="submit">Add product</button>
      </form>

      <label htmlFor="product-search">Search inventory</label>
      <input
        id="product-search"
        type="search"
        value={search}
        onChange={(event) => setSearch(event.target.value)}
        placeholder="Search by product name"
      />

      {/* Use a semantic live region to announce feedback to assistive technology. */}
      <p role="status" aria-live="polite">{message}</p>

      <section aria-label="Inventory products">
        {filteredProducts.length === 0 ? (
          <p>No matching products found.</p>
        ) : (
          filteredProducts.map((product) => (
            <ProductCard
              key={product.id}
              product={product}
              onRemove={handleRemoveProduct}
            />
          ))
        )}
      </section>
    </main>
  );
}

// Mount the application in the HTML element with id="root".
const rootElement = document.getElementById("root");

if (!rootElement) {
  throw new Error("Missing root element. Add <div id=\"root\"></div> to HTML.");
}

console.log("Starting the React inventory application.");
createRoot(rootElement).render(
  <React.StrictMode>
    <ProductInventory />
  </React.StrictMode>
);

// Note: This file contains JSX, so process it with a React-aware bundler
// such as Vite. Install React and React DOM before running the application.
Enter fullscreen mode Exit fullscreen mode

Conclusion

React best practices are less about memorizing a collection of rules and more about building predictable systems. Small components, immutable state updates, stable keys, and clearly defined responsibilities make applications easier to maintain as requirements change.

The same principle applies to performance: keep the implementation simple first, measure actual bottlenecks, and optimize where the evidence points. A well-structured application is usually easier to profile, test, and improve than one filled with premature optimizations.

Start by reviewing one existing component in your project. Remove duplicated state, simplify its responsibilities, improve its accessibility, and make its edge cases explicit. Which React best practice has made the biggest difference in your production applications?

Top comments (0)