A familiar bug that annoys every React developer
\nHave you ever seen the warning Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. in your browser console? If yes — you are not alone. This question on Stack Overflow has gathered thousands of views and has become a real headache for beginners and experienced React developers alike.
In this article, we will figure out why this error occurs, how to fix it using TypeScript, and most importantly — how to optimize the performance of your application so that such warnings no longer appear. Let's go!
\n\nWhy does React complain about an unmounted component?
\nImagine: a user opened a page, started loading data (for example, via fetch or axios), but didn't wait for the response and navigated to another screen. The component that initiated the request is already unmounted (removed from the DOM), but the asynchronous operation is still executing. When the response arrives, the code tries to update the state of a component that no longer exists. React rightly complains: "Hey, I can't update something that doesn't exist!".
In TypeScript, this problem is especially noticeable if you use strict typing and forget about cleanup functions in useEffect.
Classic example of the problem (TypeScript)
\nimport React, { useState, useEffect } from 'react';\n\ninterface User {\n id: number;\n name: string;\n}\n\nconst UserProfile: React.FC<{ userId: number }> = ({ userId }) => {\n const [user, setUser] = useState<User | null>(null);\n\n useEffect(() => {\n fetch(`/api/users/${userId}`)\n .then(res => res.json())\n .then(data => setUser(data)); // Error! The component may be unmounted\n }, [userId]);\n\n return <div>{user?.name}</div>;\n};\nIf the user quickly switches routes, the UserProfile component will be removed, and setUser will be called for nothing. We get a memory leak and an error in the console.
3 ways to fix the error (with TypeScript examples)
\nLet's look at three reliable approaches that solve the problem and improve the performance of a React application.
\n\nMethod 1: Flag using useRef (most popular)
\nCreate a ref flag that indicates whether the component is mounted. Update the state only if the flag is true.
import React, { useState, useEffect, useRef } from 'react';\n\nconst UserProfile: React.FC<{ userId: number }> = ({ userId }) => {\n const [user, setUser] = useState<User | null>(null);\n const isMounted = useRef(true);\n\n useEffect(() => {\n // Reset the flag on each mount\n isMounted.current = true;\n\n fetch(`/api/users/${userId}`)\n .then(res => res.json())\n .then(data => {\n if (isMounted.current) {\n setUser(data);\n }\n });\n\n // Cleanup: set the flag to false on unmount\n return () => {\n isMounted.current = false;\n };\n }, [userId]);\n\n return <div>{user?.name}</div>;\n};\nPros: simplicity, no additional libraries required. Cons: you must not forget about cleanup.
\n\nMethod 2: AbortController to cancel the fetch request (recommended)
\nA modern and correct approach is to cancel the request itself, rather than just ignoring the response. This saves server and client resources.
\nimport React, { useState, useEffect } from 'react';\n\nconst UserProfile: React.FC<{ userId: number }> = ({ userId }) => {\n const [user, setUser] = useState<User | null>(null);\n const [loading, setLoading] = useState(true);\n\n useEffect(() => {\n const controller = new AbortController();\n const signal = con