في تطوير React الحديث، يعد خطاف useImperativeHandle طريقة قوية لتخصيص القيمة المكشوفة للمكون ويمنح المزيد من التحكم في طرقه وخصائصه الداخلية. ونتيجة لذلك، يمكن لواجهات برمجة تطبيقات المكونات الأكثر كفاءة تحسين مرونة المنتج وقابليته للصيانة.
في هذه القطعة، تتعمق رؤى فريق Social Discovery Group في أفضل الممارسات لاستخدام useImperativeHandle بشكل فعال لتحسين مكونات React.
يوفر React العديد من الخطافات (تصف الوثائق الرسمية 17 خطافًا حتى وقت كتابة هذه السطور) لإدارة الحالة والتأثيرات والتفاعلات بين المكونات.
من بينها، useImperativeHandle هي أداة مفيدة لإنشاء واجهة برمجية (API) للمكونات الفرعية، والتي تمت إضافتها إلى React من الإصدار 16.8.0 فصاعدًا.
يتيح لك useImperativeHandle تخصيص ما سيتم إرجاعه بواسطة المرجع الذي تم تمريره إلى أحد المكونات. وهو يعمل جنبًا إلى جنب مع forwardRef، والذي يسمح بتمرير مرجع إلى أحد المكونات الفرعية.
useImperativeHandle(ref, createHandle, [deps]);
يتيح هذا الخطاف التحكم الخارجي في سلوك أحد المكونات، وهو ما قد يكون مفيدًا في مواقف معينة، مثل العمل مع مكتبات تابعة لجهات خارجية، أو رسوم متحركة معقدة، أو مكونات تتطلب الوصول المباشر إلى الأساليب. ومع ذلك، يجب استخدامه بحذر، لأنه يكسر نهج React التصريحي.
لنتخيل أننا بحاجة إلى معالجة DOM لمكون فرعي. فيما يلي مثال لكيفية القيام بذلك باستخدام مرجع.
import React, { forwardRef, useRef } from 'react'; const CustomInput = forwardRef((props, ref) => { // Use forwardRef to pass the ref to the input element return <input ref={ref} {...props} />; }); export default function App() { const inputRef = useRef(); const handleFocus = () => { inputRef.current.focus(); // Directly controlling the input }; const handleClear = () => { inputRef.current.value = ''; // Directly controlling the input value }; return ( <div> <CustomInput ref={inputRef} /> <button onClick={handleFocus}>Focus</button> <button onClick={handleClear}>Clear</button> </div> ); }
وإليك كيفية تحقيق ذلك باستخدام useImperativeHandle.
import React, { useImperativeHandle, forwardRef, useRef } from 'react'; const CustomInput = forwardRef((props, ref) => { const inputRef = useRef(); useImperativeHandle(ref, () => ({ focus: () => { inputRef.current.focus(); }, clear: () => { inputRef.current.value = ''; }, })); return <input ref={inputRef} {...props} />; }); export default function App() { const inputRef = useRef(); return ( <div> <CustomInput ref={inputRef} /> <button onClick={inputRef.current.focus}>Focus</button> <button onClick={inputRef.current.clear}>Clear</button> </div> ); }
كما هو موضح في الأمثلة أعلاه، عند استخدام useImperativeHandle، يوفر مكون الطفل للمكون الأساسي مجموعة من الأساليب التي نحددها بأنفسنا.
يسمح استخدام useImperativeHandle في السيناريوهات المتقدمة، مثل الأمثلة التي تحتوي على رسوم متحركة، بعزل السلوك المعقد داخل أحد المكونات. وهذا يجعل المكون الرئيسي أبسط وأكثر قابلية للقراءة، وخاصة عند العمل مع مكتبات الرسوم المتحركة أو الصوت.
import React, { useRef, useState, useImperativeHandle, forwardRef, memo } from "react"; import { Player } from '@lottiefiles/react-lottie-player' import animation from "./animation.json"; const AnimationWithSound = memo( forwardRef((props, ref) => { const [isAnimating, setIsAnimating] = useState(false); const animationRef = useRef(null); const targetDivRef = useRef(null); useImperativeHandle( ref, () => ({ startAnimation: () => { setIsAnimating(true); animationRef.current?.play() updateStyles("start"); }, stopAnimation: () => { animationRef.current?.stop() updateStyles("stop"); }, }), [] ); const updateStyles = (action) => { if (typeof window === 'undefined' || !targetDivRef.current) return; if (action === "start") { if (targetDivRef.current.classList.contains(styles.stop)) { targetDivRef.current.classList.remove(styles.stop); } targetDivRef.current.classList.add(styles.start); } else if (action === "stop") { if (targetDivRef.current.classList.contains(styles.start)) { targetDivRef.current.classList.remove(styles.start); } targetDivRef.current.classList.add(styles.stop); } }; return ( <div> <Player src={animation} loop={isAnimating} autoplay={false} style={{width: 200, height: 200}} ref={animationRef} /> <div ref={targetDivRef} className="target-div"> This div changes styles </div> </div> ); }) ); export default function App() { const animationRef = useRef(); const handleStart = () => { animationRef.current.startAnimation(); }; const handleStop = () => { animationRef.current.stopAnimation(); }; return ( <div> <h1>Lottie Animation with Sound</h1> <AnimationWithSound ref={animationRef} /> <button onClick={handleStart}>Start Animation</button> <button onClick={handleStop}>Stop Animation</button> </div> ); }
في هذا المثال، يقوم المكون الفرعي بإرجاع الطرق startAnimation وstopAnimation، والتي تغلف المنطق المعقد داخل نفسها.
لا يمكن ملاحظة الخطأ دائمًا على الفور. على سبيل المثال، قد يقوم المكون الرئيسي بتغيير الخصائص بشكل متكرر، وقد تواجه موقفًا حيث يستمر استخدام طريقة قديمة (ببيانات قديمة).
مثال على الخطأ:
https://use-Imperative-handle.levkovich.dev/deps-is-not-correct/wrong
const [count, setCount] = useState(0); const increment = useCallback(() => { console.log("Current count in increment:", count); // Shows old value setCount(count + 1); // Are using the old value of count }, [count]); useImperativeHandle( ref, () => ({ increment, // Link to the old function is used }), [] // Array of dependencies do not include increment function );
النهج الصحيح:
const [count, setCount] = useState(0); useImperativeHandle( ref, () => ({ increment, }), [increment] // Array of dependencies include increment function );
2. مجموعة التبعيات المفقودة
إذا لم يتم توفير مجموعة التبعيات، فسوف يفترض React أنه يجب إعادة إنشاء الكائن في useImperativeHandle في كل عملية عرض. يمكن أن يتسبب هذا في حدوث مشكلات كبيرة في الأداء، خاصةً إذا تم إجراء عمليات حسابية "ثقيلة" داخل الخطاف.
مثال على الخطأ:
useImperativeHandle(ref, () => { // There is might be a difficult task console.log("useImperativeHandle calculated again"); return { focus: () => {} } }); // Array of dependencies is missing
النهج الصحيح:
useImperativeHandle(ref, () => { // There is might be a difficult task console.log("useImperativeHandle calculated again"); return { focus: () => {} } }, []); // Array of dependencies is correct
يؤدي التعديل المباشر لملف ref.current إلى تعطيل سلوك React. إذا حاول React تحديث الملف ref، فقد يؤدي ذلك إلى تعارضات أو أخطاء غير متوقعة.
مثال على الخطأ:
useImperativeHandle(ref, () => { // ref is mutated directly ref.current = { customMethod: () => console.log("Error") }; });
النهج الصحيح:
useImperativeHandle(ref, () => ({ customMethod: () => console.log("Correct"), }));
قد يؤدي استدعاء الأساليب المقدمة عبر useImperativeHandle من useEffect أو معالجات الأحداث، على افتراض أن المرجع متاح بالفعل، إلى حدوث أخطاء - تحقق دائمًا من current قبل استدعاء أساليبه.
مثال على الخطأ:
const increment = useCallback(() => { childRef.current.increment(); }, [])
النهج الصحيح:
const increment = useCallback(() => { if (childRef.current?.increment) { childRef.current.increment() } }, [])
إذا أرجعت useImperativeHandle طرقًا تغير الحالة بشكل متزامن (على سبيل المثال، بدء الرسوم المتحركة وتعديل الأنماط في نفس الوقت)، فقد يتسبب ذلك في "فجوة" بين الحالة المرئية والمنطق الداخلي. تأكد من الاتساق بين الحالة والسلوك المرئي، على سبيل المثال، باستخدام التأثيرات (useEffect).
مثال على الخطأ:
useImperativeHandle(ref, () => ({ startAnimation: () => { setState("running"); // Animation starts before the state changes lottieRef.current.play(); }, stopAnimation: () => { setState("stopped"); // Animation stops before the state changes lottieRef.current.stop(); }, }));
النهج الصحيح:
useEffect(() => { if (state === "running" && lottieRef.current) { lottieRef.current.play(); } else if (state === "stopped" && lottieRef.current) { lottieRef.current.stop(); } }, [state]); // Triggered when the state changes useImperativeHandle( ref, () => ({ startAnimation: () => { setState("running"); }, stopAnimation: () => { setState("stopped"); }, }), [] );
يُعد استخدام useImperativeHandle مبررًا في المواقف التالية:
التحكم في سلوك مكون الطفل: على سبيل المثال، لتوفير طريقة التركيز أو إعادة التعيين لمكون إدخال معقد.
إخفاء تفاصيل التنفيذ: يتلقى المكون الرئيسي فقط الأساليب التي يحتاجها، وليس كائن المرجع بأكمله.
قبل استخدام useImperativeHandle، اسأل نفسك الأسئلة التالية:
من خلال إتقان هوك useImperativeHandle، يمكن لمطوري React إنشاء مكونات أكثر كفاءة وقابلية للصيانة من خلال عرض الأساليب بشكل انتقائي. يمكن للتقنيات التي وضعها فريق Social Discovery Group مساعدة المطورين على تحسين مرونتهم وتبسيط واجهات برمجة التطبيقات الخاصة بمكوناتهم وتحسين الأداء العام للتطبيق.
بقلم سيرجي ليفكوفيتش، مهندس برمجيات أول في مجموعة Social Discovery Group