React Pitfalls & How to Avoid Them: Best Practices for Smooth Code

Common React Development Mistakes & How to Avoid them

React Pitfalls & How to Avoid Them: Best Practices for Smooth Code

React is one of the most popular front-end JavaScript libraries for building user interfaces. Its component-based architecture and virtual DOM make it efficient. However, even seasoned React developers can face challenges that can lead to performance issues, unexpected behavior, and maintainability problems down the line. In this article, we'll shed light on some of the frequent mistakes that React developers make, along with strategies to avoid or mitigate these pitfalls.

We'll identify these pitfalls and their effective solutions to help you write clean, efficient, and scalable React applications. Whether you're new to React or an experienced practitioner, knowing these potential stumbling blocks can help you write clean, maintainable code and build more robust applications.

Let's dive into the common react pitfalls and mitigations!

Common React Pitfalls and Mitigations

Developing a React is fun, and due to the number of features React provides, it can easily make you overlook some of the common challenges. Let’s dig deeper into challenges and their mitigations.

Improper Key Management

Consequences

While using the arrays as keys in dynamic lists, rendering errors and unexpected behavior can occur, particularly when the order of Products changes. Improper key management can lead to data loss or incorrect rendering

Const Product = [
  { id: 1, name: "Product 1" },
  { id: 2, name: "Product 2" },
  { id: 3, name: "Product 3" },
];
function MyList() {
  return (
    <ul>
      {Product.map((Product, index) => (
        <li key={index}> {/* Incorrect: Uses index as key */}
          {Product. name}
        </li>
      ))}
    </ul>
  );
}

In this example, the key is set to the Product index within an array. It is important to remember that this can lead to issues when the order of Products changes. React might reuse or update the wrong components, causing unexpected behavior. So, what is the way out of this?

Mitigation

Ideally, use stable identifiers such as unique IDs to ensure that every key differs from the others in the family. In dynamically sorted lists or filtered, it is best to avoid using index numbers as keys. Maintaining the proper state of the component and facilitating quick updates are made possible by stable and distinct keys.

const Product = [
  { id: 1, name: "Product 1" },
  { id: 2, name: "Product 2" },
  { id: 3, name: "Product 3" },
];
function MyList() {
  return (
    <ul>
      {Product.map((Product) => (
        <li key={Product.id}> {/* Correct: Uses unique ID as key */}
          {Product. name}
        </li>
      ))}
    </ul>
  );
}

In the above example the key is set to the unique id property given to each product. This allows React to efficiently identify changes and update the correct components when the list order changes.

Mishandling State in Functional Components

While the functional components are rising in popularity, mishandling the state within them can lead to chaos. Treating a state like a mutable variable can cause issues, violating React's principles and potentially leading to unexpected behavior.

import React, { useState } from 'react';
function Counter() {
  const [count, setCount] = useState(0);
  const ClickCount = () => {
    count++; // This is incorrect (direct mutation)
    console.log("Count (after mutation):", count);
  };
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={ClickCount}>Increment</button>
    </div>
  );
}

In this example, the ClickCount function directly increments the count variable inside the state object. This is incorrect, as react is dependent on the same object to trigger the re-rendering. Therefore, modifying the state variable directly bypasses this mechanism.

Mitigation

To resolve this issue, you must use the useState hook to manage the particular state correctly.

import React, { useState } from 'react';
function Counter() {
  const [count, setCount] = useState(0);
  const ClickCount = () => {
    setCount(count + 1); // Correct: using state update function
    console.log("Count (after update):", count);
  };
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={ClickCount}>Increment</button>
    </div>
  );
}

In the above-mentioned code, ClickCount function uses the setCount functionality returned by the useState to create a new state object with the incremented value. This way, React is constantly aware of state changes and triggers a re-render with each updated count.

Note: For proper & predictable state management in functional components, it is advised to always use the in-built state update functions such as State or Reducer.

Directly Changing the State

In react, the state shouldn't be modified directly. While it might look like a quick fix, it can lead to unexpected behavior and debugging and testing challenges. So how can you fix it? You should always make sure to use the setState() function to ensure that the re-rendered component and virtual DOM are updated through proper channels and not directly.

Let’s take a scenario to understand it better.

Imagine that a shopping cart component are on display along with their quantities. You can be tempted to modify the object representing the cart state:

import React, { useState } from 'react';
function ShoppingCart() {
  const [cart, setCart] = useState({
    items: [
      { id: 1, name: "Grapes", quantity: 2 },
      { id: 2, name: "Kiwi", quantity: 1 },
    ],
  });
  const RemoveItem = (ProductId) => {
    // Incorrect: Directly modifying the state object
    const cartCopy = cart; // Create a copy (optional for immutability)
    const itemIndex = cartCopy.items.findIndex((item) => item.id === itemId);
    if (itemIndex !== -1) {
      cartCopy.items[itemIndex].quantity--; // Direct mutation
    }
    setCart(cartCopy);
  };
  return (
    <div>
      <h2>Shopping Cart</h2>
      <ul>
        {cart.items.map((item) => (
          <li key={item.id}>
            {item.name} - Quantity: {item.quantity}
            <button onClick={() => RemoveItem(item.id)}>Remove</button>
          </li>
        ))}
      </ul>
    </div>
  );
}

In the given example, the code is trying to update the cart state directly by adjusting the cart objects within the RemoveItem function. At first it creates the copy, which is optional, and then decreases the number of item to be removed. This approach sidesteps the React’s inbuilt mechanism of tracking state changes. So how to mitigate this? You need to make use if setState() function.

Mitigation

import React, { useState } from 'react';
function ShoppingCart() {
  const [cart, setCart] = useState({
    items: [
      { id: 1, name: "Grapes", quantity: 2 },
      { id: 2, name: "Kiwi", quantity: 1 },
    ],
  });
  const RemoveItem = (itemId) => {
    setCart((prevCart) => {
      const updatedCart = { ...prevCart }; // Create a new object
      const itemIndex = updatedCart.items.findIndex((item) => item.id === itemId);
      if (itemIndex !== -1) {
        updatedCart.items[itemIndex].quantity = Math.max(0, updatedCart.items[itemIndex].quantity - 1); // Avoid negative quantities
      }
      return updatedCart;
    });
  };
  return (
    <div>
      <h2>Shopping Cart</h2>
      <ul>
        {cart.items.map((item) => (
          <li key={item.id}>
            {item.name} - Quantity: {item.quantity}
            <button onClick={() => RemoveItem(item.id)}>Remove</button>
          </li>
        ))}
      </ul>
    </div>
  );
}

This react code shows the shopping cart component that uses the useState hook to manage the cart state. This state includes the array of items such as their IDs, names, and quantities. The RemoveItem function takes an itemid and updates the state of cart by gaining the item shown in the list and decreasing its quantity.

Utilizing Redux Too Much or Too Less

Let’s breakdown this in two categories of overusing and underusing the redux, along with the code snippets. Let’s begin with overusing the Redux.

Overutilizing the Redux

When you’re over using the redux it will make your project more complex by adding unnecessary complexities for the simple task. Imagine you have to call the crane to lift the feather, it’s pointless.

// Overly Complex Counter with Redux (unnecessary)
const countReducer = (state = { count: 0 }, action) => {
  switch (action.type) {
    case 'DECREMENT':
      return { count: state.count + 1 };
    default:
      return state;
  }
};
const store = createStore(countReducer);
function Count() {
  const dispatch = useDispatch();
  const handleClick = () => {
    dispatch({ type: 'DECREMENT' });
  };
  const count = useSelector((state) => state.count);
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={handleClick}>DECREMENT</button>
    </div>
  );
}

Mitigation

So how to ease this problem? For isolated components with basic state requirements, utilize the built-in React state management tools like useState or useReducer. When handling complex global states across several components, take Redux into consideration.

Now let’s see under using redux

Underusing Redux

Under using redux will create the difficulties in managing the complex state across the components. Also, remember that juggling the states between the multiple components can become a heavying and error-prone.

// Component A (needs access to user data)
const [user, setUser] = useState(null);
useEffect(() => {
  // Fetch user data
  fetch('/api/user')
    .then((response) => response.json())
    .then((data) => setUser(data));
}, []);
// Component B (also needs access to user data)
const [userData, setUserData] = useState(null);
useEffect(() => {
  // Fetch user data again (redundant)
  fetch('/api/user')
    .then((response) => response.json())
    .then((data) => setUserData(data));
}, []);

Mitigation

Use Redux for sophisticated global states that require sharing and multi-component access. This streamlines the logic for retrieving data and supports the state's single source of truth.

Inconsistent Whitespace

The seemingly insignificant space between the characters plays an important role in code readability and maintainability. Uneven whitespace usage can be a silent glitch in your codebase, causing confusion for you and other developers after you. Having inconsistent whitespace can create visual clutter, cause debugging challenges, and create maintenance headaches.

const Mydemo = () => (  // Missing closing parenthesis
  <div>
<h1>Hello World!</h1>  // Inconsistent indentation
    <button  onClick={() => console.log("Clicked!")}>  // Extra space
      Click Me
    </button>
  </div>
);

Their code might still work, but the inconsistent indentation, missing closing parenthesis, and extra space make it hard to read and visually unappealing. So, how can you solve this issue? Let’s find out!

Mitigation

To maintain the white space usage consistency, you can go for the code formatter such as ESLint or Prettier. These tools have predefined rules for the whitespaces, this ensures that you’re sticking to the chosen style guide. Moreover, establish there should be a team agreement for the whitespace. The agreement should cover the aspects such as indentation styles and spacings. Having a same consistency throughout the codebase facilitates the smoother collaboration and comprehensive approach.

Using Duplicate State

React’s efficiency in rendering the components based on state changes can be compromised when duplicate states are used. It can lead to the debugging challenges and unexpected behavior. This duplicate state rises when the same state information is copied and used in different places. Let’s understand it better with the example.

Let’s say if a component manages the username and displays it in both the header and footer of the profile using different variables, this is called duplicate state. This can lead to the inconsistency, debugging challenges and Redundancy. Take a look at the code below:

function UProfile() {
  const [userName, setUserName] = useState('Jane Doe');
  const [pName, setProfileName] = useState(' Jane Doe '); // Duplicate state
  const NameChange = (newName) => {
    setUserName(newName); // Updates header name
  };
  return (
    <div>
      <h1>Hello, {userName}!</h1>  {/* Header */}
      <ProfileEditor onChange={NameChange} />  {/* Profile section */}
    </div>
  );
}

The UProfile component defined by this React function unintentionally maintains two state variables that appear to be the same, userName and profileName, leading to duplicate state. Only the userName state—likely meant for the header display—needs to be updated by the NameChange function. The NameChange function is likely passed as a prop to the ProfileEditor component, which updates the user's name, and the component renders a <h1> element with the userName shown.

Mitigation

function UProfile() {
  const [name, setName] = useState('Jane Doe');
  const NameChange = (newName) => {
    setName(newName);
  };
  return (
    <div>
      <h1>Hello, {name}! </h1> {/* Header */}
      <ProfileEditor onChange={NameChange} /> {/* Profile section */}
    </div>
  );
}

When multiple components necessitate the same state information, it's advisable to elevate the state to a shared parent component. This practice centralizes state management in one location, guaranteeing uniform updates across all components.

Key Takeaways

React development thrives on avoiding common development pitfalls. By prioritizing, immutability, and component reusability, and efficient state management empowers team to build a robust and scalable react application with the smooth user experience.