Today, I will share some common errors encountered in React development and how to avoid them. Understanding the details behind these issues can help prevent similar mistakes.

Updating State After Component Unmounts

Error message: Can't perform a React state update on an unmounted component

This error occurs when a state update is triggered for a component that has already been unmounted. In other words, we cannot set state after a component is destroyed to prevent memory leaks.

const Component = () => {
const [data, setData] = useState(null);
useEffect(() => {
fetchAsyncData().then((data) => setData(data));
// ...
});
};

For example, when fetching data, if you navigate to another page while the request is still in progress, the component might be unmounted before the data is fetched. There are two ways to solve this problem:

  • Cancel asynchronous requests when the component unmounts

    The first method (recommended) is to cancel asynchronous requests when the component unmounts. Some libraries provide methods to cancel asynchronous requests. If you are not using a third-party library, you can use AbortController to cancel the request. This essentially cancels the side effect when the component unmounts:

    const Component = () => {
    const [data, setData] = useState(null);
    useEffect(() => {
    const controller = new AbortController();
    fetch(url, { signal: controller.signal }).then((data) => setData(data));
    return () => {
    controller.abort();
    };
    });
    };
  • Track whether the component is mounted

    Alternatively, you can track the mounted state of the component. If it is not mounted or has been unmounted, return false; otherwise, return true.

    const Component = () => {
    const [data, setData] = useState(null);
    const isMounted = useRef(true);
    useEffect(() => {
    fetchAsyncData().then((data) => {
    if (isMounted.current) {
    setData(data);
    }
    });
    return () => {
    isMounted.current = false;
    };
    }, []);
    // ...
    };

    However, this method is not recommended as it retains references to unmounted components, which can lead to memory leaks and performance issues.

Not Using Keys When Rendering Lists

Error message: Warning: Each child in a list should have a unique key prop

One of the most common tasks in React development is rendering components by iterating over arrays. You can embed this logic into your components using Array.map in JSX and return the required components in the callback.

import { Card } from "./Card";
const data = [
{ id: 1, text: "JavaScript" },
{ id: 2, text: "TypeScript" },
{ id: 3, text: "React" },
];
export default function App() {
return (
<div className="container">
{data.map((content) => (
<div className="card">
<Card text={content.text} />
</div>
))}
</div>
);
}

This will result in a warning: Warning: Each child in a list should have a unique key prop, indicating that each generated component needs a unique key. Therefore, add a key value to the outermost element returned by the map callback. This value should be a string or number and should be unique within the list of components.

export default function App() {
return (
<div className="container">
{data.map((content) => (
<div key={content.id} className="card">
<Card text={content.text} />
</div>
))}
</div>
);
}

Although not following this requirement will not cause the application to crash, it may lead to unexpected situations. React uses these keys to determine which children in the list have changed and uses this information to decide which parts of the previous DOM can be reused and which parts need to be recalculated during re-rendering. Therefore, it is recommended to add keys.

Incorrect Order of Hook Calls

Error message: React Hook "useXXX" is called conditionally. React Hooks must be called in the exact same order in every component render

Let's look at the following code:

const Toggle = () => {
const [isOpen, setIsOpen] = useState(false);
if (isOpen) {
return <div></div>;
}
const openToggle = useCallback(() => setIsOpen(true), []);
return <button onClick={}></button>;
};

When isOpen is true, it directly returns the div element. This causes the order of calling the useCallback hook to be inconsistent when isOpen is true and false. React will warn us: React Hook "useCallback" is called conditionally. React Hooks must be called in the exact same order in every component render. This is actually what the React official documentation says: Do not call Hooks inside loops, conditions, or nested functions. Ensure that they are always called at the top level of the React function and before any return.

To modify the above code:

const Toggle = () => {
const [isOpen, setIsOpen] = useState(false);
const openToggle = useCallback(() => setIsOpen(true), []);
if (isOpen) {
return <div></div>;
}
return <button onClick={}></button>;
};

Missing Dependencies in useEffect

Error message: React Hook useEffect has a missing dependency: 'XXX'. Either include it or remove the dependency array

Let's look at the example given by the React official website:

function Example() {
function doSomething() {
console.log(someProp);
}
useEffect(() => {
doSomething();
}, []);
}

Defining an empty dependency array in useEffect is unsafe because the doSomething function uses someProp. This will result in an error: React Hook useEffect has a missing dependency: 'XXX'. Either include it or remove the dependency array. When someProp changes, the result of the doSomething function will also change, but since the dependency array is empty, the callback will not be executed.

There are two ways to solve this problem:

  1. Declare the function needed in useEffect. This method is suitable for functions that only need to be called once, such as initialization functions:

    function Example() {
    useEffect(() => {
    function doSomething() {
    console.log(someProp);
    }
    doSomething();
    }, [someProp]);
    }
  2. Use useCallback to define dependencies, ensuring that the function body will change when its own dependencies change:

    function Example() {
    const doSomething = useCallback(() => {
    console.log(someProp);
    }, [someProp]);
    useEffect(() => {
    doSomething();
    }, [doSomething]);
    }

Too Many Re-renders

Error message: Too many re-renders. React limits the number of renders to prevent an infinite loop

This error means there are too many re-renders. React limits the number of renders to prevent an infinite loop. This situation often occurs when a component has too many state updates in a short period of time. The most common reasons for infinite loops are:

  • Directly executing state updates in rendering.
  • Not providing appropriate callbacks to event handlers.

If you encounter this warning, you can check these two aspects of the component:

const Component = () => {
const [count, setCount] = useState(0);
setCount(count + 1); // State update in rendering
return (
<div className="App">
<button onClick={() => setCount((prevCount) => prevCount + 1)}>Increment that counter</button>
</div>
);
};

Rendering an Object as a Single Data Item

Error message: Objects are not valid as a React child / Functions are not valid as a React child

In React, there are many things we can render to the DOM, such as HTML tags, JSX elements, raw JavaScript values, JavaScript expressions, etc. However, objects and functions cannot be rendered to the DOM because these values will not resolve to meaningful values. If you render an object or function, you will get the above error. The solution to this problem is simple: check whether the rendered content is a valid value.

const Component = ({ body }) => (
<div>
<h1>{/* */}</h1>
<div className="body">{body}</div>
</div>
);

Adjacent JSX Elements Not Wrapped in an Enclosing Tag

Error message: Adjacent JSX elements must be wrapped in an enclosing tag

This error means that adjacent JSX elements must be wrapped in an enclosing tag, which means there must be a root element:

const Component = () => (
<Nice />
<Bad />
);

From the perspective of React developers, this component will only be used inside another component. Therefore, in their mental model, it makes sense to return two elements from a component because the generated DOM structure will be the same, regardless of whether the external element is defined in this component or in the parent component. However, React cannot make this assumption. The component might be used at the root level and break the application because it would result in an invalid DOM structure.

Therefore, you should always wrap multiple JSX elements returned by a component in an enclosing tag. It can be an element, a component, or a React Fragment:

const Component = () => (
<React.Fragment>
<Nice />
<Bad />
</React.Fragment>
);

Or you can directly use an empty tag to wrap two JSX elements:

const Component = () => (
<>
<Nice />
<Bad />
</>
);

Using Old State

Let's look at an example of a counter:

const Increaser = () => {
const [count, setCount] = useState(0);
const increase = useCallback(() => {
setCount(count + 1);
}, [count]);
const handleClick = () => {
increase();
increase();
increase();
};
return (
<>
<button onClick={handleClick}>+</button>
<div>Counter: {count}</div>
</>
);
};

The handleClick method will execute three operations to increase the state variable count when the button is clicked. So, does clicking the button increase it by 3? Not quite. After clicking the button, count will only increase by 1. The problem is that when we click the button, it is equivalent to the following operations:

const handleClick = () => {
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
};

When the first setCount(count + 1) is called, there is no problem, and it will update count to 1. However, for the second and third calls to setCount, count still uses the old state (count is 0), so it will also calculate count as 1. The reason for this is that state variables are updated in the next render.

To solve this problem, use a functional approach to update the state:

const Increaser = () => {
const [count, setCount] = useState(0);
const increase = useCallback(() => {
setCount((count) => count + 1);
}, [count]);
const handleClick = () => {
increase();
increase();
increase();
};
return (
<>
<button onClick={handleClick}>+</button>
<div>Counter: {count}</div>
</>
);
};

After making this change, React will get the latest value, and each click will increase the count by 3. So remember: if you want to use the current state to calculate the next state, use the functional form to update the state: setValue(prevValue => prevValue + someResult).

Conclusion

In this article, we have discussed some common errors encountered in React development and how to avoid them. Understanding the details behind these issues can help prevent similar mistakes.