خلل مألوف يزعج كل مطور React
\nهل سبق لك أن رأيت في وحدة تحكم المتصفح تحذيرًا 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.؟ إذا كان الأمر كذلك - فأنت لست وحدك. هذا السؤال على Stack Overflow حصل على آلاف المشاهدات وأصبح صداعًا حقيقيًا للمبتدئين والمطورين ذوي الخبرة في React.
في هذه المقالة، سنفهم لماذا يحدث هذا الخطأ، وكيفية إصلاحه باستخدام TypeScript، والأهم من ذلك - كيفية تحسين أداء تطبيقك بحيث لا تظهر هذه التحذيرات مرة أخرى. هيا بنا!
\n\nلماذا يصرخ React بشأن المكون غير المركب؟
\nتخيل: فتح المستخدم صفحة، وبدأ تحميل البيانات (على سبيل المثال، عبر fetch أو axios)، لكنه لم ينتظر الرد وانتقل إلى شاشة أخرى. المكون الذي بدأ الطلب قد تم إلغاء تركيبه بالفعل (أُزيل من DOM)، لكن العملية غير المتزامنة لا تزال قيد التنفيذ. عندما يصل الرد، يحاول الكود تحديث حالة (state) مكون غير موجود. يحتج React بحق: "مرحبًا، لا يمكنني تحديث ما هو غير موجود!".
في TypeScript، تكون هذه المشكلة ملحوظة بشكل خاص إذا كنت تستخدم كتابة صارمة وتنسى وظائف التنظيف (cleanup) في useEffect.
مثال كلاسيكي للمشكلة (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)); // خطأ! قد يكون المكون غير مركب\n }, [userId]);\n\n return <div>{user?.name}</div>;\n};\nإذا قام المستخدم بتبديل المسار بسرعة، سيتم حذف المكون UserProfile، وسيتم استدعاء setUser في غير محله. نحصل على تسرب للذاكرة وخطأ في وحدة التحكم.
3 طرق لإصلاح الخطأ (مع أمثلة على TypeScript)
\nدعنا نستعرض ثلاث طرق موثوقة تحل المشكلة وتحسن أداء تطبيق React.
\n\nالطريقة 1: علامة باستخدام useRef (الأكثر شيوعًا)
\nننشئ علامة ref توضح ما إذا كان المكون مركبًا. نقوم بتحديث الحالة فقط إذا كانت العلامة 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 // إعادة تعيين العلامة عند كل تركيب\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 // التنظيف: تعيين العلامة إلى false عند إلغاء التركيب\n return () => {\n isMounted.current = false;\n };\n }, [userId]);\n\n return <div>{user?.name}</div>;\n};\nالإيجابيات: البساطة، لا يتطلب مكتبات إضافية. السلبيات: يجب ألا تنسى التنظيف (cleanup).
\n\nالطريقة 2: AbortController لإلغاء طلب fetch (موصى به)
\nالنهج الحديث والصحيح هو إلغاء الطلب نفسه، وليس مجرد تجاهل الرد. هذا يوفر موارد الخادم والعميل.
\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