React Pitfalls & How to Avoid Them: Best Practices for Smooth Code
Common React Development Mistakes & How to Avoid them
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.