Loading...

Loading...
August 19, 2026

In this story, we will understand which best practices we need to follow to become a good developer in React
In React, managing state immutably is considered a best practice because it ensures predictability, improves performance, and simplifies debugging. React relies heavily on detecting changes in state or props to trigger re-renders. If state is mutated directly, React may not detect the changes as expected, which can lead to unexpected behavior.
Immutability means that instead of directly modifying an object or array, you create a new version of it with the necessary updates. This approach helps maintain a clear history of state changes, making it easier to debug and enabling features like undo/redo functionality.
Consider a scenario where you have an array of tasks in your component’s state, and you need to add a new task:
const [tasks, setTasks] = useState(["Task 1", "Task 2"]);function addTask() {
tasks.push("Task 3"); // Direct mutation
setTasks(tasks); // React may not detect this change
}In this example, the push method modifies the original array directly, which React may not recognize as a state update. As a result, the component may not re-render as expected.
To handle state updates immutably, you create a new array using methods like concat, the spread operator, or map:
function addTask() {
const newTasks = [...tasks, "Task 3"];
setTasks(newTasks); // React detects the change and re-renders
}Here, the spread operator creates a new array that includes all existing tasks along with the new task. React recognizes the new array as a distinct state and triggers a re-render.
If your state contains an object and you need to update a specific property, avoid mutating the object directly:
const [user, setUser] = useState({ name: "John", age: 25 });function updateUserName() {
user.name = "Jane"; // Direct mutation
setUser(user); // React may not detect this change
}Instead, use the spread operator or Object.assign to create a new object:
function updateUserName() {
const updatedUser = { ...user, name: "Jane" };
setUser(updatedUser); // React detects the change
}This approach ensures that the state is updated immutably, allowing React to properly identify the changes.
Predictable Behavior: When state is managed immutably, it becomes easier to predict the outcome of updates. Each update creates a new state, leaving the previous state unchanged.
Performance Optimization: React’s reconciliation process relies on shallow comparisons to determine what needs to be updated. Immutable updates make these comparisons faster and more reliable.
Debugging and Testing: Immutable updates help maintain a history of state changes, simplifying debugging and enabling time-travel debugging in tools like Redux DevTools.
By adhering to immutability principles, you ensure that your React application remains robust, efficient, and easier to maintain. Always remember to create new instances of state rather than mutating existing ones.
Here’s a handy React best practice: Derived values don’t need to be stored in state! 🎯 If a value can be calculated directly from existing props or state, compute it on the fly during render instead of creating additional state variables.
For example, instead of storing a formatted date string in state, you can calculate it dynamically:
const formattedDate = new Date(date).toLocaleDateString();Here, formattedDate is derived from date and recalculated every time the component renders. This approach is efficient because it avoids managing extra state and ensures that the formatted value always reflects the latest data.
Simplifies Component Logic:
No need to manage or sync additional state for derived values.
Avoids Unnecessary State Updates:
Derived values are recalculated only when the source data changes, reducing potential re-renders.
Promotes Functional Programming:
Encourages a clean, declarative style by deriving values directly within the render logic.
Instead of using state to store the formatted date, derive it directly from a date prop:
function DateDisplay({ date }) {
const formattedDate = new Date(date).toLocaleDateString();
return <p>The formatted date is: {formattedDate}</p>;
}If you need to filter items based on a search term, you can derive the filtered list directly from items and searchQuery:
function ItemList({ items, searchQuery }) {
const filteredItems = items.filter(item =>
item.toLowerCase().includes(searchQuery.toLowerCase())
);
return (
<ul>
{filteredItems.map(item => (
<li key={item}>{item}</li>
))}
</ul>
);
}If you have a list of prices, calculate the total dynamically:
function PriceSummary({ prices }) {
const total = prices.reduce((sum, price) => sum + price, 0);
return <p>Total: ${total.toFixed(2)}</p>;
}Key Takeaway
When you can derive a value from existing data, calculate it during render instead of storing it in state. This keeps your components simpler, cleaner, and easier to maintain!
In React, the principle of “Compute Values Without Effects” refers to the practice of calculating derived or computed values in a clean and predictable manner, without causing side effects such as updating state, triggering re-renders, or modifying external data. This approach improves the readability, maintainability, and predictability of your code.
React’s declarative nature encourages separating computation from rendering. By avoiding side effects during computations, your components remain pure and easier to test. Side effects, like API calls or modifying the DOM, should be handled in specific lifecycle hooks (e.g., useEffect) or dedicated logic outside of the computation itself. This ensures that computations are deterministic: given the same input, they always produce the same output.
Suppose you have a list of numbers, and you want to compute their sum and display it. Instead of using useEffect or modifying state directly during computation, you can derive the value inline or using a memoized function with useMemo.
import React, { useMemo } from 'react';
const NumberList = ({ numbers }) => {
// Derived computation without side effects
const total = useMemo(() => {
return numbers.reduce((sum, num) => sum + num, 0);
}, [numbers]);
return (
<div>
<h2>Total: {total}</h2>
<ul>
{numbers.map((num, index) => (
<li key={index}>{num}</li>
))}
</ul>
</div>
);
};
export default NumberList;In this example:
The total is computed using useMemo to avoid unnecessary recalculations during re-renders.
There are no side effects in the computation itself — it only returns the computed value.
Key Takeaways
Compute Inline or Memoize: Use functions or
useMemoto derive values directly from props or state without triggering state updates.Avoid State for Derived Data: Do not store derived data in state unless it is expensive to compute and shared between components.
Separate Side Effects: Use
useEffectonly for side effects, such as fetching data or manipulating the DOM—not for computing values.
In React.js, keys should be unique because they are essential for identifying elements in a list and ensuring efficient updates to the DOM. When rendering lists, React uses keys to determine which items have changed, been added, or removed. Without unique keys, React cannot reliably track elements, leading to potential rendering issues, reduced performance, or unexpected bugs. For instance, if a key is reused or missing, React might mistakenly reuse a DOM node for a different element, resulting in incorrect behavior or visual glitches.
const fruits = ['Apple', 'Banana', 'Orange'];
function FruitList() {
return (
<ul>
{fruits.map((fruit) => (
<li key={fruit.length}>{fruit}</li> // Using `fruit.length` is not unique
))}
</ul>
);
}Here, if two fruits have the same length (e.g., “Apple” and “Mango”), the keys will clash, causing React to mismanage the elements.
const fruits = [
{ id: 1, name: 'Apple' },
{ id: 2, name: 'Banana' },
{ id: 3, name: 'Orange' },
];
function FruitList() {
return (
<ul>
{fruits.map((fruit) => (
<li key={fruit.id}>{fruit.name}</li> // Using a unique `id`
))}
</ul>
);
}In this example, each li element is uniquely identified by the id property of the fruit object. This allows React to efficiently update the DOM when the list changes.
By ensuring keys are unique and stable (unchanging between renders), you allow React to optimize rendering and maintain a seamless user experience.
In React, the useEffect hook is a powerful tool for managing side effects in function components, such as fetching data, updating the DOM, or subscribing to external systems. However, a common pitfall is omitting dependencies from the dependency array, leading to subtle bugs, unexpected behaviors, or performance issues. Understanding why it's crucial to include all dependencies in the array helps create more predictable and maintainable code.
The dependency array in useEffect specifies when the effect should re-run. React will compare the current values of the dependencies with their values from the previous render. If any value has changed, the effect will re-run. Omitting dependencies can cause the effect to rely on outdated values or miss changes to variables that it should react to.
Consider the following code snippet:
function Example({ userId }) {
const [userData, setUserData] = React.useState(null);
React.useEffect(() => {
fetch(`/api/user/${userId}`)
.then(response => response.json())
.then(data => setUserData(data));
}, []); // Missing userId dependency
return <div>{userData ? userData.name : "Loading..."}</div>;
}In this example, the useEffect hook fetches user data. However, the dependency array is empty, so the effect will only run once when the component mounts. If the userId prop changes, the component will not fetch the new user data, leading to stale information being displayed. Adding userId to the dependency array ensures the effect re-runs whenever userId changes:
React.useEffect(() => {
fetch(`/api/user/${userId}`)
.then(response => response.json())
.then(data => setUserData(data));
}, [userId]); // Correctly includes userId as a dependencyWhen functions are used inside an effect, they should also be included in the dependency array. However, functions are recreated on every render, which can cause the effect to re-run unnecessarily. To address this, wrap the function in React.useCallback:
function Example() {
const [count, setCount] = React.useState(0);
const logCount = React.useCallback(() => {
console.log(`Current count is ${count}`);
}, [count]); // Ensures the function always reflects the latest count
React.useEffect(() => {
logCount();
}, [logCount]); // Include the function as a dependency
return <button onClick={() => setCount(count + 1)}>Increment</button>;
}Stale Closures: Variables captured in closures may have outdated values.
Missed Updates: Reactivity can break if dependencies aren’t tracked, leading to bugs.
Performance Issues: Overly broad dependency arrays, such as omitting useCallback or useMemo, can cause unnecessary re-renders or computations.
By always including dependencies in useEffect and addressing potential pitfalls like stale closures and redundant re-renders, you can create more predictable, efficient, and maintainable React components. Linting tools like eslint-plugin-react-hooks can further assist by warning you about missing dependencies.
Here’s a pro tip: Don’t rush to use useEffect. 🙅♂️ It’s powerful but can lead to messy code if overused. React frameworks provide solutions to manage side effects more elegantly. For data fetching, consider libraries like TanStack Query or SWR that handle requests and caching efficiently, leading to a better user experience.
Alternative strategies:
Derive values directly.
Respond to events with handlers.
React is a robust library, but knowing how to use it effectively can make all the difference. These lessons are just the beginning.
Having an in-depth idea about in and outs of any technology helps you during development and optimization.
React Js is the perfect library for modern development it has everything to offer for development and optimization
Thanks for reading, and happy coding! 🎉