React is a popular javascript framework that is the most loved library for building user interfaces. Despite being one of the front-end developer's favorites, React introduces pitfalls that beginners often fall into. These blogs are related to state and effects and other functionalities that make React easier to work with. Here are five common mistakes beginners often make in React, along with their solutions.
1. Mutating state
React's state is an essential concept that allows components to manage and update their data. However, directly modifying the state is a common mistake that can lead to unexpected behavior and difficult-to-debug issues.
An example of a mutating state is through the use of destructive array methods such as push
to add an item to a list.
const [items, setItems] = useState([1, 2, 3]);
// ❌ Mutates an existing object
const addItem = (item) => {
items.push(item); // Mutating state directly
setItems(items);
};
// ✅ Creates a new object
const addItem = (item) => {
setItems([...items, item]); // Creating a new array
};
React relies on a state variable's identity to tell when the state has changed. When we push an item into an array, we aren't changing that array's identity, and so React can't tell that the value has changed.
Instead of modifying an existing array, I'm creating a new one from scratch. It includes all of the same items (courtesy of the ... spread syntax), as well as the newly-entered item.
The distinction here is between editing an existing item, versus creating a new one. When we pass a value to a state-setter function like setCount, it needs to be a new entity. The same thing is true for objects.
2. Accessing state after changing it
State in React is asynchronous. This means that when we update the state we are not re-assigning a variable but rather scheduling a state update. Mostly common, developers will be frustrated if they console.log
the new value and it is not there.
For example:
const [count, setCount] = useState(0);
const increment = () => {
setCount(count + 1);
console.log(count); // This will log the old count, not the new one
};
This can lead to confusion when debugging because the console might not show the expected value. It can take a while for us to fully wrap our heads around this idea, but here's something that might help it click: we can't reassign the count
variable, because it's a constant!
To work with the updated state, you can use the functional form of setState
, which provides the latest state:
const increment = () => {
setCount((prevCount) => {
console.log(prevCount + 1); // Correctly logs the new count
return prevCount + 1;
});
};
3. Using Functions as useEffect Dependencies
useEffect
is one of the most abused hooks and developers often use it recklessly even in areas where it is not needed. One common mistake with useEffect
is failing to provide a dependency array, which leads to endless renders.
One point to note here is not to confuse re-renders with refresh. Since React uses a Virtual DOM, re-renders happen in the back-stage and thus developers might fail to notice.
The following is a react useEffect example:
function App(){
const [data, setData] = useState(null);
const fetchData = () => {
// some code
}
useEffect(() => {
fetchData(); //used inside useEffect
}, [fetchData])
}
It is not recommended to define a function outside and call it inside an effect. In the above case, the passed dependency is a function, and a function is an object, so fetchData is called on every render.
React compares the fetchData from the previous render and the current render, but the two aren't the same, so the call is triggered.
4. Props Drilling
Props drilling in React occurs when developers pass the same props to every component one level down from its parent to the required component at the end. Thus, components become closely connected with each other and can’t be used without adding a particular prop. The fewer unnecessary interconnections between components in your code, the better.
For example, if we need to pass a user name
to a deeply nested component but the name is defined in the first parent component:
const Grandparent = () => {
const user = { name: 'Alice' };
return <Parent user={user} />;
};
const Parent = ({ user }) => {
return <Child user={user} />;
};
const Child = ({ user }) => {
return <div>{user.name}</div>;
};
In this example, the user
prop is passed down from Grandparent
to Child
through the Parent
component, even though Parent
doesn't need it. This kind of pattern can quickly lead to a tangled mess of props.
To avoid props drilling, consider using React Context or state management libraries like Redux, Zustand, or Recoil. Context allows you to share data across multiple components without passing props manually through every level.
// Create a context
const UserContext = createContext();
const Grandparent = () => {
const user = { name: 'Alice' };
return (
<UserContext.Provider value={user}>
<Parent />
</UserContext.Provider>
);
};
const Parent = () => {
return <Child />;
};
const Child = () => {
const user = useContext(UserContext);
return <div>{user.name}</div>;
};
5. Changing from Uncontrolled to Controlled Inputs
Uncontrolled components rely on the DOM to manage their state, while controlled components rely on React. Beginners sometimes switch from uncontrolled to controlled components without a clear need, adding complexity without gaining any tangible benefits.
One major reason why developers switch to controlled inputs is the need for validations. However, we can validate inputs without the need for additional javascript simply using browser inbuilt functions. See more here a-better-way-to-validate-html-forms-without-usestate.
Another reason (I am often guilty of this) is when we need to implement search functionality that filters our data to return the results. However, the most efficient solution to search is to store the value in search parameters. In this way, the value will not be lost on refresh.
Developers might implement the search functionality like this:
function SearchForm() {
const [query, setQuery] = useState(null);
function handleSearch (e){
e.preventDefault()
window.location.href=`/my-site/search?q=${query}`
}
return (
<form onSubmit={handleSearch}>
<search>
<input
type="search"
value={search}
onChange={() => setQuery(e.target.value)}></input>
</search>
</form>
);
}
This example receives the search input and saves it in the state. When the search form is submitted, we redirect the user to the search page with the search-params.
One simple solution is:
function SearchForm() {
return (
<form action="/search/query">
<search>
<input type="search" name="query"></input>
</search>
</form>
);
}
This will work the same and the form will be submitted with the query params that can be used to filter the data. Another advantage of this setup is that if we are using NextJS
, this form will work as a server component
and thus benefit from faster rendering.
Conclusion
There are many mistakes that we make in React that we are often unaware of. By understanding and avoiding these common mistakes, you can write more robust and efficient React code. Remember to practice good coding practices and explore the full range of React hooks to build high-quality user interfaces.
the don
Published on
React 19 is helping fix such issues with a new compiler that will eliminate the need to use useMemo and also show errors in a more clear and understandable way.
Tutor Juliet
Published on
One common mistake is choosing react in the first place
the don
• Oct 28, 2024Tech Wizard
Published on
Generally, "prop drilling" is fine. It looks a bit ugly, but it's easy to understand and easy to change. I think we can use prop drilling with some caution especially in server components since using context will turn our entire component tree to a client component.
Tech Tales Team
Published on
Let me know which mistake you often make with React
the don
• Oct 28, 2024