The Most Common Mistakes When Using React

Updated on · 5 min read
The Most Common Mistakes When Using React

Answering React-related questions on Stack Overflow, I've noticed that there are a few main categories of issues people have with the library. I've decided to write about the most common ones and show how to handle them in hopes that it'll be helpful to those new to React or anyone in general, who's struggling with its basic concepts. Both pitfalls of using Class-based components and Functional components that use hooks are covered interchangeably.

Directly modifying the state

The state in React is considered immutable and therefore should not be directly changed. A special setState method and the setter function from the useState hook should be used instead. Consider the following example, where you'd want to update the checked field of a particular object in an array, based on the state of a checkbox.

javascript
const updateFeaturesList = (e, idx) => { listFeatures[idx].checked = e.target.checked; setListFeatures(listFeatures); };
javascript
const updateFeaturesList = (e, idx) => { listFeatures[idx].checked = e.target.checked; setListFeatures(listFeatures); };

The issue with this code is that the changes to the state won't be reflected in the UI since the state is updated with the same object reference and therefore it doesn't trigger a re-render. Another important reason for not mutating the state directly is that due to its asynchronous nature later state updates might override the ones made directly to the state, resulting in some evasive bugs. The correct way in this case would be to use the setter method of useState.

javascript
const updateFeaturesList = (e, idx) => { const { checked } = e.target; setListFeatures(features => { return features.map((feature, index) => { if (idx === index) { feature = { ...feature, checked }; } return feature; }); }); };
javascript
const updateFeaturesList = (e, idx) => { const { checked } = e.target; setListFeatures(features => { return features.map((feature, index) => { if (idx === index) { feature = { ...feature, checked }; } return feature; }); }); };

By using map and object spread we're also making sure that we're not changing the original state items.

Setting wrong value types on the initial state

Setting the initial state values to null or an empty string and then accessing properties of that value in render as if it's an object is quite a common mistake. The same goes for not providing default values for nested objects and then trying to access them in render or other component methods.

jsx
class UserProfile extends Component { constructor(props) { super(props); this.state = { user: null }; } componentDidMount() { fetch("/api/profile").then(data => { this.setState({ user: data }); }); } render() { return ( <div> <p>User name:</p> <p>{this.state.user.name}</p> // Cannnot read property 'name' of null </div> ); } }
jsx
class UserProfile extends Component { constructor(props) { super(props); this.state = { user: null }; } componentDidMount() { fetch("/api/profile").then(data => { this.setState({ user: data }); }); } render() { return ( <div> <p>User name:</p> <p>{this.state.user.name}</p> // Cannnot read property 'name' of null </div> ); } }

A similar error happens with setting the value on an initial state to an empty array and then trying to access the n-th item from it. While the data is being fetched by an API call, the component will be rendered with provided initial state, and trying to access a property on a null or undefined element will cause an error. Therefore, it is important to have the initial state closely represent the updated state. In our case a correct state initialisation is as follows:

jsx
class UserProfile extends Component { constructor(props) { super(props); this.state = { user: { name: "" // Define other fields as well } }; } componentDidMount() { fetch("/api/profile").then(data => { this.setState({ user: data }); }); } render() { return ( <div> <p>User name:</p> <p>{this.state.user.name}</p> // Renders without errors </div> ); } }
jsx
class UserProfile extends Component { constructor(props) { super(props); this.state = { user: { name: "" // Define other fields as well } }; } componentDidMount() { fetch("/api/profile").then(data => { this.setState({ user: data }); }); } render() { return ( <div> <p>User name:</p> <p>{this.state.user.name}</p> // Renders without errors </div> ); } }

From the UX point of view, it's probably best to display some sort of loader until the data is fetched.

Forgetting that setState is asynchronous

Another common mistake is trying to access the state value right after setting it.

javascript
handleChange = count => { this.setState({ count }); this.props.callback(this.state.count); // Old state value };
javascript
handleChange = count => { this.setState({ count }); this.props.callback(this.state.count); // Old state value };

Setting new value doesn't happen immediately, normally it's done on the next available render, or can be batched to optimise performance. So accessing a state value after setting it might not reflect the latest updates. This issue can be fixed by using an optional second argument to setState, which is a callback function, called after the state has been updated with its latest values.

javascript
handleChange = count => { this.setState({ count }, () => { this.props.callback(this.state.count); // Updated state value }); };
javascript
handleChange = count => { this.setState({ count }, () => { this.props.callback(this.state.count); // Updated state value }); };

It's quite different with the hooks though since the setter function from useState doesn't have a second callback argument akin to that of setState. In this case, the officially recommended way is to use the useEffect hook.

javascript
const [count, setCount] = useState(0) useEffect(() => { callback(count); // Will be called when the value of count changes }, [count, callback]); const handleChange = value => { setCount(value) };
javascript
const [count, setCount] = useState(0) useEffect(() => { callback(count); // Will be called when the value of count changes }, [count, callback]); const handleChange = value => { setCount(value) };

It should be noted that setState is not asynchronous in a way that it returns a promise. So slapping async/await on it or using then won't work (another common mistake).

Incorrectly relying on the current state value for calculating the next state

This issue is related to the one discussed above as it also has to do with the state update being asynchronous.

javascript
handleChange = count => { this.setState({ count: this.state.count + 1 }); // Relying on current value of the state to update it };
javascript
handleChange = count => { this.setState({ count: this.state.count + 1 }); // Relying on current value of the state to update it };

The issue with this approach is that the value of the count may not be properly updated at the moment when the new state is being set, which will result in the new state value being set incorrectly. A correct way here is to use the functional form of setState.

javascript
increment = () => { this.setState(state => ({ count: state.count + 1 })); // The latest state value is used };
javascript
increment = () => { this.setState(state => ({ count: state.count + 1 })); // The latest state value is used };

The functional form of setState has a second argument - props at the time the update is applied, which can be used in a similar way as a state.

The same logic applies to the useState hook, where the setter accepts a function as an argument.

javascript
const increment = () => { setCount(currentCount => currentCount + 1) };
javascript
const increment = () => { setCount(currentCount => currentCount + 1) };

Omitting dependency array for useEffect

This is one of the less popular mistakes but happens nevertheless. Even though there are completely valid cases for omitting the dependency array for useEffect, doing so when its callback modifies the state might cause an infinite loop.

Passing objects or other values of non-primitive type to the useEffect's dependency array

Similar to the case above, but a more subtle mistake, is tracking objects, arrays, or other non-primitive values in the effect hook's dependency array. Consider the following code.

javascript
const features = ["feature1", "feature2"]; useEffect(() => { // Callback }, [features]);
javascript
const features = ["feature1", "feature2"]; useEffect(() => { // Callback }, [features]);

Here when we pass an array as a dependency, React will store only the reference to it and compare it to the previous reference of the array. However, since it is declared inside the component, the features array is recreated on every render, meaning that its reference will be a new one every time, thus not equal to the one tracked by useEffect. Ultimately, the callback function will be run on each render, even if the array hasn't been changed. This is not an issue with primitive values, like strings and numbers, since they are compared by value and not by reference in JavaScript.

There are a few ways to fix this. The first option is to move variable declaration outside the component, so it won't be recreated every render. However, in some cases, this is not possible, for example, if we're tracking props or tracked dependency is a part of the component's state. Another option is to use a custom deep compare hook to properly track the dependency references. An easier solution would be to wrap the value into the useMemo hook, which would keep the reference during re-renders.

javascript
const features = useMemo(() => ["feature1", "feature2"], []); useEffect(() => { // Callback }, [features]);
javascript
const features = useMemo(() => ["feature1", "feature2"], []); useEffect(() => { // Callback }, [features]);

Hopefully, this list will help you to avoid the most common React issues and improve your understanding of the main pitfalls.

P.S.: Are you looking for a reliable and user-friendly hosting solution for your website or blog? Cloudways is a managed cloud platform that offers hassle-free and high-performance hosting experience. With 24/7 support and a range of features, Cloudways is a great choice for any hosting needs.