الفرق بين المراجعتين لصفحة: «React/hooks faq»

من موسوعة حسوب
تحديث
تحديث
 
(3 مراجعات متوسطة بواسطة نفس المستخدم غير معروضة)
سطر 280: سطر 280:
إذا كنت تريد عن قصد قراءة أحدث حالة من رد نداء غير متزامن، فيمكنك تخزينها في مرجع، ثمّ تعديلها وقرائتها.
إذا كنت تريد عن قصد قراءة أحدث حالة من رد نداء غير متزامن، فيمكنك تخزينها في مرجع، ثمّ تعديلها وقرائتها.


أخيرًا، هناك سبب آخر محتمل لظهور خاصيات أو حالة قديمة، وهو إن كنت تستخدم تحسينا "لمصفوفة التبعية"، ولكنك لم تحدد جميع التبعيات بصورة صحيحة. على سبيل المثال، إذا حدّد تأثير ما المصفوفة الفارغة <code>[]</code> كوسيط ثان، بيد أنّه حاول قراءة خاصية ما بداخله، فسيظل "يرى" القيمة الأولية لتلك الخاصية. الحل هو إما إزالة مصفوفة التبعية، أو إصلاحها. إليك كيفية التعامل مع الدوال، وهذه استراتيجيات أخرى شائعة لتنفيذ التأثيرات بمعدل أقل دون تخطي التبعيات بشكل غير صحيح.
أخيرًا، هناك سبب آخر محتمل لظهور خاصيات أو حالة قديمة، وهو إن كنت تستخدم تحسينا "لمصفوفة التبعية"، ولكنك لم تحدد جميع التبعيات بصورة صحيحة. على سبيل المثال، إذا حدّد تأثير ما المصفوفة الفارغة <code>[]</code> كوسيط ثان، بيد أنّه حاول قراءة خاصية ما بداخله، فسيظل "يرى" القيمة الأولية لتلك الخاصية. الحل هو إما إزالة مصفوفة التبعية، أو إصلاحها. إليك [[React/hooks faq#.D9.87.D9.84 .D9.85.D9.86 .D8.A7.D9.84.D8.A2.D9.85.D9.86 .D8.AD.D8.B0.D9.81 .D8.A7.D9.84.D8.AF.D9.88.D8.A7.D9.84 .D9.85.D9.86 .D9.82.D8.A7.D8.A6.D9.85.D8.A9 .D8.A7.D9.84.D8.AA.D8.A8.D8.B9.D9.8A.D8.A7.D8.AA.D8.9F|كيفية التعامل مع الدوال،]] [[React/hooks faq#.D9.85.D8.A7 .D8.A7.D9.84.D8.B0.D9.8A .D8.B9.D9.84.D9.8A .D9.81.D8.B9.D9.84.D9.87 .D8.A5.D8.B0.D8.A7 .D9.83.D8.A7.D9.86.D8.AA .D8.AA.D8.A8.D8.B9.D9.8A.D8.A7.D8.AA .D8.A7.D9.84.D8.AA.D8.A3.D8.AB.D9.8A.D8.B1 .D8.AA.D8.|وهذه استراتيجيات أخرى]] شائعة لتنفيذ التأثيرات بمعدل أقل دون تخطي التبعيات بشكل غير صحيح.


'''ملاحظة''': لقد قدمنا [https://github.com/facebook/react/issues/14920 قاعدة ESLint شاملة للتبعيات] كجزء من حزمة <code>[https://www.npmjs.com/package/eslint-plugin-react-hooks#installation eslint-plugin-React-hooks]</code>. والتي تطلق تحذيرا في حال تحديد التبعيات بصورة غير صحيحة وتقترح حلا لإصلاح الخلل.
'''ملاحظة''': لقد قدمنا [https://github.com/facebook/react/issues/14920 قاعدة ESLint شاملة للتبعيات] كجزء من حزمة <code>[https://www.npmjs.com/package/eslint-plugin-react-hooks#installation eslint-plugin-React-hooks]</code>. والتي تطلق تحذيرا في حال تحديد التبعيات بصورة غير صحيحة وتقترح حلا لإصلاح الخلل.
سطر 315: سطر 315:
=== أيمكنني إنشاء مرجع إلى مكون دالة؟ ===
=== أيمكنني إنشاء مرجع إلى مكون دالة؟ ===
رغم أنّه لا يجب أن تحتاج إلى تنفيذ ذلك في أغلب الأحيان، قد تعرض بعض التوابع الأمرية على مكون أب (parent component) مع الخطاف <code>[[React/hooks reference#useImperativeHandle|useImperativeHandle]]</code>.
رغم أنّه لا يجب أن تحتاج إلى تنفيذ ذلك في أغلب الأحيان، قد تعرض بعض التوابع الأمرية على مكون أب (parent component) مع الخطاف <code>[[React/hooks reference#useImperativeHandle|useImperativeHandle]]</code>.
=== كيف يمكنني قياس عقدة DOM؟ ===
إحدى أبسط الطرق لقياس موضع أو حجم عقدة DOM هي استخدام [[React/refs and the dom#.D8.B1.D8.AF.D9.88.D8.AF .D9.86.D8.AF.D8.A7.D8.A1 .D8.A7.D9.84.D9.85.D8.B1.D8.A7.D8.AC.D8.B9|ردود نداء المراجع]]. سوف تستدعي React رد النداء كلما تم ربط المرجع بعقدة مختلفة. إليك [https://codesandbox.io/s/l7m0v5x4v9 المثال التوضيحي] التالي:<syntaxhighlight lang="javascript">
function MeasureExample() {
  const [height, setHeight] = useState(0);
  const measuredRef = useCallback(node => {
    if (node !== null) {
      setHeight(node.getBoundingClientRect().height);
    }
  }, []);
  return (
    <>
      <h1 ref={measuredRef}>Hello, world</h1>
      <h2>The above header is {Math.round(height)}px tall</h2>
    </>
  );
}
</syntaxhighlight>لم نختر <code>useRef</code> في هذا المثال لأن مرجع الكائن لا يُعلمنا بالتغييرات في قيمة المرجع الحالية. يضمن استخدام [[React/refs and the dom#.D8.B1.D8.AF.D9.88.D8.AF .D9.86.D8.AF.D8.A7.D8.A1 .D8.A7.D9.84.D9.85.D8.B1.D8.A7.D8.AC.D8.B9|ردود نداء المراجع]] أنه حتى في حال [https://codesandbox.io/s/818zzk8m78 قام أحد المكونات الفرعية بعرض العقدة المقاسة لاحقًا] (على سبيل المثال استجابةً لنقرة) ، فسنتلقى إخطارًا بشأنها في المكون الأب ويمكننا تحديث القياسات.
لاحظ أننا مررنا <code>[]</code> كمصفوفة تبعية لاستخدامها في <code>useCallback</code>. هذا يضمن أن رد نداء المرجع الخاص بنا لن يتغير بين عمليات إعادة التصيير، وبالتالي فلن تستدعيها React دون داع.
في هذا المثال، لن يُستدعى  رد نداء المرجع إلا عندما يوصل المكوّن أو يُفصل، نظرًا لأن المكوّن <code><nowiki><h1></nowiki></code> المُصيّر يظل موجودًا في جميع عمليات إعادة التصيير. إذا كنت تريد أن يتم إعلامك في كل مرة يتم فيه تغيير حجم أحد المكونات، فقد ترغب في استخدام <code>[https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver ResizeObserver]</code>أو خطاف مبني عليها من طرف ثالث.
يمكنك استعمال [https://codesandbox.io/s/m5o42082xy هذا المثال] في خطاف قابل لإعادة الاستخدام:<syntaxhighlight lang="javascript">
function MeasureExample() {
  const [rect, ref] = useClientRect();
  return (
    <>
      <h1 ref={ref}>Hello, world</h1>
      {rect !== null &&
        <h2>The above header is {Math.round(rect.height)}px tall</h2>
      }
    </>
  );
}
function useClientRect() {
  const [rect, setRect] = useState(null);
  const ref = useCallback(node => {
    if (node !== null) {
      setRect(node.getBoundingClientRect());
    }
  }, []);
  return [rect, ref];
}
</syntaxhighlight>


=== ما الذي يعينه <code>const [thing, setThing] = useState()‎</code>؟ ===
=== ما الذي يعينه <code>const [thing, setThing] = useState()‎</code>؟ ===
سطر 323: سطر 372:
=== أيمكنني تخطي تأثير ما في عمليات التحديث؟ ===
=== أيمكنني تخطي تأثير ما في عمليات التحديث؟ ===
نعم. اطلع على قسم "[[React/hooks reference#.D8.AA.D9.86.D9.81.D9.8A.D8.B0 .D8.AA.D8.A3.D8.AB.D9.8A.D8.B1 .D8.B4.D8.B1.D8.B7.D9.8A.D9.8B.D9.91.D8.A7|تنفيذ تأثير شرطيًّا]]". لاحظ أنَّ نسيان معالجة تحديثات يولد غالبًا أخطاء، إذ هذا هو سبب عدم كون هذا السلوك هو السلوك الافتراضي.
نعم. اطلع على قسم "[[React/hooks reference#.D8.AA.D9.86.D9.81.D9.8A.D8.B0 .D8.AA.D8.A3.D8.AB.D9.8A.D8.B1 .D8.B4.D8.B1.D8.B7.D9.8A.D9.8B.D9.91.D8.A7|تنفيذ تأثير شرطيًّا]]". لاحظ أنَّ نسيان معالجة تحديثات يولد غالبًا أخطاء، إذ هذا هو سبب عدم كون هذا السلوك هو السلوك الافتراضي.
=== هل من الآمن حذف الدوال من قائمة التبعيات؟ ===
بشكل عام، الجواب هو لا.<syntaxhighlight lang="javascript">
function Example({ someProp }) {
  function doSomething() {
    console.log(someProp);
  }
  useEffect(() => {
    doSomething();
  }, []); // 🔴 `someProp` والتي تستعمل `doSomething`  هذا ليس آمنا، إذ تستدعي
}
</syntaxhighlight>من الصعب تذكر الخاصيات أو الحالة التي تستخدمها الدوال خارج التأثير. لأجل هذا يجب التصريح بالدوال التي يحتاجها التأثير بداخله. سيسهّل ذلك عليك معرفة القيم من نطاق المكون الذي يعتمد عليه التأثير:<syntaxhighlight lang="javascript">
function Example({ someProp }) {
  useEffect(() => {
    function doSomething() {
      console.log(someProp);
    }
    doSomething();
  }, [someProp]); // ✅  `someProp` جيد، فالتأثير لا يستعمل إلا
}
</syntaxhighlight>إن لم تستخدم عقب هذا أي قيم من نطاق المكون، فمن الآمن إذن تمرير <code>[]</code>:<syntaxhighlight lang="javascript">
useEffect(() => {
  function doSomething() {
    console.log('hello');
  }
  doSomething();
}, []); // ✅ لا بأس في هذه الحالة لأننا لم نستخدم أي قيم من نطاق المكون
</syntaxhighlight>اعتمادًا على حالة الاستخدام، أمامك عدة خيارات سنوضحها أدناه.
'''ملاحظة''': لقد قدمنا [https://github.com/facebook/react/issues/14920 قاعدة ESLint شاملة للتبعيات] كجزء من حزمة <code>[https://www.npmjs.com/package/eslint-plugin-react-hooks#installation eslint-plugin-React-hooks]</code>. والتي تطلق تحذيرا في حال تحديد التبعيات بصورة غير صحيحة وتقترح حلا لإصلاح الخلل.
دعونا نرى لماذا هذا مهم.
إذا مررت [[React/hooks reference#.D8.AA.D9.86.D9.81.D9.8A.D8.B0 .D8.AA.D8.A3.D8.AB.D9.8A.D8.B1 .D8.B4.D8.B1.D8.B7.D9.8A.D9.8B.D9.91.D8.A7|قائمة من التبعيات]] كوسيط أخير للدوال <code>useEffect</code> أو <code>useLayoutEffect</code> أو <code>useMemo</code> أو <code>useCallback</code> أو <code>useImperativeHandle</code> ، فيجب أن تتضمن جميع القيم المستخدمة داخل رد النداء والمشاركة في تدفق بيانات React. ويشمل ذلك الخاصيات والحالة وأي شيء مشتق منهما.
ليس من الآمن حذف دالة من قائمة التبعيات إلا إن كنت متأكدا أنه لا شيء فيها (أو الدوال المُستدعاة من قبلها) يشير إلى خاصيات أو حالة أو قيم مشتقة منهما.  المثال التالي به خلل:<syntaxhighlight lang="javascript">
function ProductPage({ productId }) {
  const [product, setProduct] = useState(null);
  async function fetchProduct() {
    const response = await fetch('http://myapi/product/' + productId); // Uses productId prop
    const json = await response.json();
    setProduct(json);
  }
  useEffect(() => {
    fetchProduct();
  }, []); // 🔴 `productId` تستعمل `fetchProduct` غير صالح، لأنّ
  // ...
}
</syntaxhighlight>أفضل طريقة لإصلاح هذا الخلل هي نقل الدالة إلى داخل تأثيرك. هذا يسهل معرفة الخاصيات أو الحالة التي يستخدمها التأثير، والتأكد من أنّه تم التصريح بها جميعًا:<syntaxhighlight lang="javascript">
function ProductPage({ productId }) {
  const [product, setProduct] = useState(null);
  useEffect(() => {
    // عبر نقل هذه الدالة إلى داخل التأثير، يمكننا رؤية القيم التي يستخدمها
    async function fetchProduct() {
      const response = await fetch('http://myapi/product/' + productId);
      const json = await response.json();
      setProduct(json);
    }
    fetchProduct();
  }, [productId]); // ✅ productId صالح، لأن التأثير لا يستخدم إلا
  // ...
}
</syntaxhighlight>يتيح لك هذا أيضًا التعامل مع الاستجابات غير المتوقعة باستخدام متغير محلي داخل التأثير:<syntaxhighlight lang="javascript">
useEffect(() => {
    let ignore = false;
    async function fetchProduct() {
      const response = await fetch('http://myapi/product/' + productId);
      const json = await response.json();
      if (!ignore) setProduct(json);
    }
    fetchProduct();
    return () => { ignore = true };
  }, [productId]);
</syntaxhighlight>لقد نقلنا الدالة إلى داخل التأثير بحيث لا يلزم أن تكون في قائمة التبعيات الخاصة به.
'''ملاحظة''': راجع هذا [https://codesandbox.io/s/jvvkoo8pq3 المثال التوضيحي] و[https://www.robinwieruch.de/react-hooks-fetch-data/ هذه المقالة] لمعرفة المزيد عن كيفية جلب البيانات باستخدام الخطافات.
إذا لم تتمكن لسبب ما من نقل الدالة إلى داخل التأثير، فهناك بعض الخيارات المتاحة:
* يمكنك محاولة نقل تلك الدالة إلى خارج المكون. في هذه الحالة، لن يشير المكون إلى أي خاصيات أو حالة، ولا يلزم أيضًا أن يكون في قائمة التبعيات.
* إذا كانت الدالة التي تستدعيها محصورة على إجراء عمليات حسابية وكان استدعاؤها آمنا أثناء التصيير، فيمكنك استدعاؤها خارج التأثير بدلاً من ذلك، وجعل التأثير يعتمد على القيمة المُعادة.
* كحل أخير، يمكنك إضافة الدالة إلى تبعيات التأثير ولكن مع تغليف تعريفها في خطاف <code>useCallback</code>. يضمن ذلك ألا تتغير عند كل تصيير إلا إن تغيرت تبعياتها أيضا:
<syntaxhighlight lang="javascript">
function ProductPage({ productId }) {
  // ✅ لتجنب التغيير عن كل تصيير useCallback غلف باستخدام
  const fetchProduct = useCallback(() => {
    // ... Does something with productId ...
  }, [productId]); // ✅ useCallback تم تحديد جميع تبعيات
  return <ProductDetails fetchProduct={fetchProduct} />;
}
function ProductDetails({ fetchProduct }) {
  useEffect(() => {
    fetchProduct();
  }, [fetchProduct]); // ✅useCallback تم تحديد جميع تبعيات
  // ...
}
</syntaxhighlight>لاحظ أنه في المثال أعلاه، احتجنا إلى إبقاء الدالة في قائمة التبعيات. يضمن ذلك أن يؤدي التغيير في خاصية <code>productId</code> الخاصة بـ <code>ProductPage</code> تلقائيًا إلى بدء عملية إعادة الجلب في المكون <code>ProductDetails</code>.
=== ما الذي علي فعله إذا كانت تبعيات التأثير تتغير كثيرًا؟ ===
في بعض الأحيان، قد يستخدم التأثير حالة تتغير كثيرًا. قد ترغب في حذف تلك الحالة من قائمة التبعيات، ولكن ذلك قد يؤدي إلى أخطاء:<syntaxhighlight lang="javascript">
function Counter() {
  const [count, setCount] = useState(0);
  useEffect(() => {
    const id = setInterval(() => {
      setCount(count + 1); // `count` يعتمد هذا التأثير على حالة
    }, 1000);
    return () => clearInterval(id);
  }, []); // 🔴 في التبعيات `count` هناك خلل، إذ لم يتم تحديد
  return <h1>{count}</h1>;
}
</syntaxhighlight>مجموعة التبعيات الفارغة <code>[]</code> تعني أنّ التأثير لن يُنفّذ إلا مرة واحدة عندما يتم وصل المكون، وليس عند كل إعادة تصيير. تكمن المشكلة في أنه داخل رد نداء <code>setInterval</code>، لا تتغير قيمة <code>count</code>، لأننا أنشأنا تعبيرا مغلقا (closure) مع ضبط قيمة <code>count</code> عند <code>0</code> كما كان عند تنفيذ رد نداء التأثير. في كل ثانية، يستدعي رد النداء <code>setCount (0 + 1)‎</code> ، لذلك لا يتجاوز <code>count</code> القيمة<code>1</code> أبدًا.
سيؤدي تمرير <code>[count]</code> كقائمة للتبعيات إلى إصلاح الخلل، ولكنه سيؤدي إلى إعادة ضبط المجال عند كل تغيير.  عمليا، سيُنفذ كل <code>setInterval</code> مرة واحدة قبل أن يُمحى (على غرار <code>setTimeout</code>.) قد لا يكون ذلك مرغوبًا. لإصلاح ذلك يمكننا استخدام [[React/hooks reference#useState|التحديث الدالي لـ <code>setState</code>]]. والذي يتيح لنا تحديد كيفية تغيير الحالة دون الرجوع إلى الحالة الراهنة:<syntaxhighlight lang="javascript">
function Counter() {
  const [count, setCount] = useState(0);
  useEffect(() => {
    const id = setInterval(() => {
      setCount(c => c + 1); // ✅ `count` لا يتعلق هذا بالمتغير
    }, 1000);
    return () => clearInterval(id);
  }, []); // ✅  لا يستخدم التأثير أي متغير في نطاق المكون
  return <h1>{count}</h1>;
}
</syntaxhighlight>(هوية الدالة <code>setCount</code> ستبقى ثابتة، لذا من الآمن حذفها.)
يُنفّذ رد النداء <code>setInterval</code> مرة واحدة كل ثانية، ولكن في كل مرة يمكن للاستدعاء الداخلي لـ <code>setCount</code> استخدام قيمة مُحدثة لـ <code>count</code>  (تسمى <code>c</code> في رد النداء هنا.)
في الحالات المعقدة (مثلا، إن كانت حالة ما تعتمد على حالة أخرى)، حاول نقل منطق تحديث الحالة خارج التأثير باستخدام الخطاف <code>useReducer</code>. تقدم [https://adamrackis.dev/state-and-use-reducer/ هذه المقالة] مثالاً عن كيفية القيام بذلك. تبقى هوية الدالة <code>dispatch</code>  من <code>useReducer</code> ثابتة دائمًا - حتى لو تم التصريح عن دالة الاختزال (reducer) داخل المكون، مع قراءة خاصياته.
كملاذ أخير، إذا أردت استخدام شيء يشبه <code>this</code>  في الأصناف، يمكنك استخدام مرجع لتخزين متغير قابل للتغيير. ثم يمكنك كتابته أو قرائته كما يبين المثال التالي:<syntaxhighlight lang="javascript">
function Example(props) {
  //  اخفظ آحدث الخاصيات في المرجع
  const latestProps = useRef(props);
  useEffect(() => {
    latestProps.current = props;
  });
  useEffect(() => {
    function tick() {
      //  اقرأ أحدث خاصية في أي وقت
      console.log(latestProps.current);
    }
    const id = setInterval(tick, 1000);
    return () => clearInterval(id);
  }, []); // لا يعاد تنفيذ هذا التأثير أبدا
}
</syntaxhighlight>لا تفعل هذا إلا إن لم يكن لديك بديل أفضل، ذلك أن الاعتماد على العناصر القابلة للتغيير يصعّب التنبؤ بسلوك المكونات. إذا صادفت نمطا غامضا، فقم [https://github.com/facebook/react/issues/new بتقديم التماس] مرفوقا بمثال قابل للتنفيذ، وسنحاول المساعدة.


=== كيف يمكنني تنفيذ <code>shouldComponentUpdate</code>؟ ===
=== كيف يمكنني تنفيذ <code>shouldComponentUpdate</code>؟ ===
سطر 460: سطر 678:
   const textRef = useRef();
   const textRef = useRef();


   useLayoutEffect(() => {
   useEffect(() => {
     textRef.current = text; // كتابتها إلى المرجع
     textRef.current = text; // كتابتها إلى المرجع
   });
   });
سطر 476: سطر 694:
   );
   );
}
}
</syntaxhighlight>هذا النمط معقدٌ كثيرًا ولكن يبدو أنَّك لا بأس من فعل ذلك كمخرج هروب لتحسين الأداء إن شعرت أنك بحاجة إليه. يصبح التعامل مع هذا النمط أسهل وأكثير تقبُّلًا إن استخرجته إلى [[React/hooks custom|خطاف مخصص]]:<syntaxhighlight lang="javascript">
</syntaxhighlight>هذا النمط معقدٌ كثيرًا ولكن يبدو أنَّك لا بأس من فعل ذلك كمخرج هروب لتحسين الأداء إن شعرت أنك بحاجة إليه. يصبح التعامل مع هذا النمط أسهل وأكثير تقبُّلًا إن استخرجته إلى [[React/hooks custom|خطاف مخصص]]:<syntaxhighlight lang="javascript">
function Form() {
function Form() {
سطر 497: سطر 716:
   });
   });


   useLayoutEffect(() => {
   useEffect(() => {
     ref.current = fn;
     ref.current = fn;
   }, [fn, ...dependencies]);
   }, [fn, ...dependencies]);
سطر 506: سطر 725:
   }, [ref]);
   }, [ref]);
}
}
</syntaxhighlight>في كلا الحالتين، لا ننصح باستعمال هذا النمط، ودافع ذكره هنا هو للتكملة فقط. عوضًا عنه، يفضل تجنب تمرير ردود نداء بشكل عميق للأسفل.
</syntaxhighlight>في كلا الحالتين، لا ننصح باستعمال هذا النمط، ودافع ذكره هنا هو للتكملة فقط. عوضًا عنه، يفضل تجنب تمرير ردود نداء بشكل عميق للأسفل.



المراجعة الحالية بتاريخ 16:11، 7 نوفمبر 2020

الخطافات هي إضافة جديدة إلى الإصدار 16.8 في React، إذ تسمح لك باستعمال ميزة الحالة وميزات React الأخرى دون كتابة أي صنف.

تجيب هذه الصفحة عن بعض الأسئلة التي يتكرر طرحها حول الخطافات.

خطة تبني الخطافات

أي إصدار من React يتضمن الخطافات؟

بدءًا من الإصدار 16.8.0، تضمنت React تنفيذًا مستقرًا للخطافات من أجل:

لاحظ أنه لتمكين الخطافات، ينبغي أن تكون جميع حزم React من الإصدار 16.8.0 أو أعلى. لن تعمل الخطافات إذا نسيت تحديث شجرة React DOM مثلا.

تدعم ReactNative 0.59 وما بعدها الخطافات دعمًا كاملًا.

هل احتاج إلى إعادة كتابة جميع مكونات الأصناف الخاصة بي؟

لا. لا يوجد أية خطط مستقبلية لحذف الأصناف من React. ستبقى الأصناف مضمنة في React، إذ لا يمكن تحمُّل عبء إعادة كتابة الشيفرات من جديد. جلَّ ما ننصح به هو تجريب الخطافات في الشيفرات الجديدة.

ما الذي يمكنني فعله مع الخطافات ولا يمكنني فعله مع الأصناف؟

توفر الخطافات وسيلةً جديدةً تتسم بالقوة لإعادة استعمال دالة (وظيفة ما) بين المكونات. توثيق "بناء خطاف خاص بك" يعطيك لمحةً عن ما يمكن فعله مع الخطافات. تشرح هذه المقالة الي نشرها أحد أفراد فريق React الأساسيين بنظرة تفصيلية الآفاق التي فُتحَت أمامنا بإضافة الخطافات إلى React.

ما هي نسبة المعرفة التي بقيت على صلة بـ React فيما يخص الخطافات؟

الخطافات هي طريقةٌ مباشرةٌ لاستعمال ميزات React التي تعرفها مسبقًا مثل الحالة، ودورة الحياة، والسياق، والمراجع (refs). إنها لم تغيِّر بشكل أساسي كيفية عمل React، ومعرفتك بالمكونات والخاصيات، وتدفق البيانات من الأعلى إلى الأسفل (top-down data flow) ستبقى كما هي ولن يغيَّر أي شيء.

تمتلك الخطافات منحى تعليمي خاص بها فقط. إن كان هنالك أي شيء ناقص في هذا التوثيق، أنشئ مشكلةً على GitHub وسنبذل قصارى جهدنا لمساعدتك.

أيتوجب علي استعمال الخطافات، أم الأصناف، أم كلاهما؟

نشجع على البدء بتجريب الخطافات واستعمالها في مكوناتك الجديدة عندما تشعر أنك جاهز لذلك. احرص على موافقة كل فرد من أفراد فريقك أيضًا على استعمالها بعد أن يكونوا قد اطلعوا على كامل توثيق الخطافات. لا ننصح بإعادة كتابة الأصناف الموجودة وتحويلها إلى خطافات إلا إذا كنت قد خططت مسبقًا لفعل ذلك (أي لإصلاح مشكلة مثلًا أو لغرض آخر).

لا تستطيع استعمال الخطافات داخل مكون صنف، ولكن يمكنك بالتأكيد المزج بين الأصناف ومكونات دالة مع الخطافات في شجرة واحدة. سواءً كان مكونٌ ما صنفًا أو دالةً، فإنَّ تلك الخطافات المستعملة هي تفاصيل التنفيذ لذلك المكون. نتوقع على المدى البعيد أن تصبح الخطافات الوسيلة الرئيسية التي يستعملها الجميع في كتلة مكونات React.

هل تغطي الخطافات جميع حالات الاستخدام التي توفرها الأصناف؟

هدفنا من الخطافات هو أن تغطي جميع حالات استخدام الأصناف في أقرب وقت ممكن. ليس هنالك أي خطاف مكافئ لدورتي الحياة getSnapshotBeforeUpdate و componentDidCatch الغير شائعتين بعد؛ لا تقلق، إذ ستغطي الخطافات هذه الناحية قريبًا.

ما زالت الخطافات حديثة العهد، وقد لا تتوافق بعض المكتبات الموفرة من طرف ثالث معها في الوقت الحالي.

هل تستبدل الخطافات خاصيات التصيير والمكونات ذات الترتيب الأعلى؟

غالبًا، خاصيات التصيير والمكونات ذات الترتيب الأعلى تُصيَّر ابنًا واحدًا فقط. نعتقد أنَّ الخطافات هي وسيلةٌ بسيطةٌ لتخدم حالة الاستخدام هذه. لا يزال هنالك متسعٌ لكلا النمطين (قد يملك مكون scroller افتراضي مثلًا الخاصية renderItem أو قد يملك مكون container حاوي على هيكل DOM خاصة به)؛ ولكن في معظم الحالات، ستكون الخطافات كافية ويمكنها أن تساعد في تقليل التشعب في شجرتك.

ما الذي تعينه الخطافات بالنسبة للواجهات البرمجية الشهيرة مثل connect()‎ في مكتبة Redux ومكتبة React Router؟

يمكنك الاستمرار باستعمال الواجهات البرمجية نفسها التي تستعملها عادةً، إذ ستستمر بالعمل دون أية مشكلات.

تدعم React Redux واجهة الخطافات البرمجية منذ الإصدار 7.1.0 وتكشف خطافات مثلuseDispatch أو useSelector.

تدعم React Router واجهة الخطافات البرمجية منذ الإصدار 5.1

قد تدعم باقي المكتبات الخطافات مستقبلا.

هل تعمل الخطافات مع أنواع البيانات الثابتة (static typing)؟

صُمِّمَت الخطافات مع أخذ الأنواع الثابتة بالحسبان. لمَّا كانت الخطافات دوالًا، فإنَّها أسهل للكتابة الصحيحة من أنماط أخرى مثل المكونات ذات المستوى الأعلى. تتضمن أحدث تعريفات للأداة Flow و TypeScript في React دعمًا للخطافات.

الأهم من ذلك أنَّ الخطافات المخصصة تمنحك القوة لتقييد واجهة React البرمجية إن أردت كتابتها بشكل صارم بطريقة ما. توفر لك React الأنواع الأساسية (primitives). ولكن يمكنك الدمج بينها بطرائق عدة أكثر من الطرائق الغير تقليدية التي وفرناها.

كيف يمكن اختبار المكونات التي تستعمل الخطافات؟

من وجهة نظر React، المكونات التي تستعمل الخطافات هي مكونات عادية تمامًا. إن لم يكن خيار الاختبار الخاص بك يعتمد على DOM الافتراضي وكائنات React الداخلية (أي React internals)، يجب ألا تختلف عملية اختبار المكونات مع الخطافات عن تلك التي اعتدت على استعمالها عادةً لاختبار المكونات.

ملاحظة: تتضمن صفحة وصفات الاختبار العديد من الأمثلة التي يمكنك نسخها ولصقها.

على سبيل المثال، دعنا نفترض أنه لدينا مكون العداد (counter) التالي:

function Example() {
  const [count, setCount] = useState(0);
  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });
  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

سنختبره باستعمال ReactDOM. للتأكد من تطابق السلوك مع الذي يحصل في المتصفح، سنغلف عملية تصيير وتحديث الشيفرة في استدعاءات ReactTestUtils.act()‎:

import React from 'react';
import ReactDOM from 'react-dom';
import { act } from 'react-dom/test-utils';
import Counter from './Counter';

let container;

beforeEach(() => {
  container = document.createElement('div');
  document.body.appendChild(container);
});

afterEach(() => {
  document.body.removeChild(container);
  container = null;
});

it('can render and update a counter', () => {
  // اختبر أول تصيير وتأثير
  act(() => {
    ReactDOM.render(<Counter />, container);
  });
  const button = container.querySelector('button');
  const label = container.querySelector('p');
  expect(label.textContent).toBe('You clicked 0 times');
  expect(document.title).toBe('You clicked 0 times');

  // اختبر ثاني تصيير وتأثير
  act(() => {
    button.dispatchEvent(new MouseEvent('click', {bubbles: true}));
  });
  expect(label.textContent).toBe('You clicked 1 times');
  expect(document.title).toBe('You clicked 1 times');
});

تطبِّق الاستدعاءات act()‎ أيضًا التأثيرات داخلها.

إن أردت اختبار خطاف مخصص، يمكنك فعل ذلك عبر إنشاء مكون في اختبارك، واستعمال ذلك الخطاف منه. وتستطيع بعدئذٍ اختبار المكون الذي كتبته.

لتقليل الشيفرة المتداولة (boilerplate)، نوصي باستعمال المكتبة react-testing-library التي صُمِّمَت لتشجيع كتابة اختبارات تستعمل مكوناتك كما سيفعل المستخدم النهائي.

ما الذي يحصل بالضبط عند فرض تطبيق قواعد إضافة تصحيح الأخطاء ESLint؟

نوفر الإضافة ESLint التي تفرض تطبيق القواعد الخاصة بالخطافات عليها لتجنب حصول أية أخطاء. إنها تفترض أنَّ أية دالة تبدأ بالسابقة use ثم يليها حرف كبير هي خطاف. نحن ندرك أنَّ هذه الطريقة للتعرف على الخطافات ليست مثالية وقد يكون هنالك بعض الإيجابيات الزائفة (false positives)، ولكن بدون عُرفٍ منتشر في بيئة العمل (ecosystem-wide convention)، ليس هنالك أية وسيلة لجعل الخطافات تعمل بشكل صحيح. أضف إلى ذلك أنَّ الأسماء الطويلة ستحبِّط الآخرين من إمَّا تبني الخطافات واستعمالها أو اتباع ما هو متعارف عليه.

باختصار، فرض تطبيق القواعد هي:

  • استدعاء الخطافات إمَّا داخل دالة PascalCase (افترض كونها مكونًا) أو دالة useSomething أخرى (افترض كونها خطافًا مخصصًا).
  • تُستدعَى الخطافات بالترتيب نفسه في كل عملية تصيير.

هنالك بضعة أساليب كشف (heuristics) أخرى وقد تتغير مع مرور الوقت، إذ نضبط القاعدة ونصيغها لتحقيق التوازن بين إيجاد الأخطاء وتجنب الإيجابيات الزائفة.

من الأصناف إلى الخطافات

كيف تتوافق توابع دورة الحياة مع الخطافات؟

  • constructor: لا تحتاج مكونات دالة إلى باني. يمكنك تهيئة الحالة عبر استدعاء الخطاف useState. إن كان حسابها يشكل عبئًا على الأداء، فيمكنك تمرير دالة إلى useState.
  • getDerivedStateFromProps: تحل مكان جدولة تحديثٍ أثناء التصيير.
  • shouldComponentUpdate: اطلع على React.memo في الأسفل.
  • render: هذا التابع هو جسم مكون الدالة نفسها.
  • componentDidMount، و componentDidUpdate، و componentWillUnmount: يحل الخطاف useEffect مكان هذه التوابع بشتى أشكال دمجها مع بعضها (بما فيها الحالات النادرة).
  • componentDidCatch، و getDerivedStateFromError: ليس هنالك أي خطاف مكافئ لهذين التابعين بعد، ولكن سيُضَاف في القريب العاجل.

كيف يمكنني جلب البيانات باستخدام الخطافات؟

إليك مثالا توضيحيا صغيرا لتبدأ به. لمعرفة المزيد، راجع هذه المقالة حول جلب البيانات باستخدام الخطافات.

هل هنالك شيء شبيه بمتغيرات النسخة؟

نعم. الخطاف useRef()‎ ليس مخصص لمراجع DOM فقط. الكائن ref هو حاوية عامة (generic container)، إذ الخاصية current فيه قابلةٌ للتعديل وتستطيع تخزين أية قيمة بشكل مشابه لنسخة أية خاصية في صنف ما. يمكنك الوصول إليها من داخل useEffect:

function Timer() {
  const intervalRef = useRef();

  useEffect(() => {
    const id = setInterval(() => {
      // ...
    });
    intervalRef.current = id;
    return () => {
      clearInterval(intervalRef.current);
    };
  });

  // ...
}

إن أردنا ضبط فترة زمنية (interval)، لن نحتاج إلى المرجع ref (المُعرِّف id يمكن أن يكون محليًّا نسبةً للتأثير)، ولكن من المفيد لو أردنا تصفير الفترة الزمنية من داخل معالج الحدث:

  // ...
  function handleCancelClick() {
    clearInterval(intervalRef.current);
  }
  // ...

نظريًّا، يمكنك أن تتخيل المراجع وكأنها مشابهة لمتغيرات نسخة في أي صنف. إن لم تكن تُجرِي عملية تهيئة كسولة، تجنب ضبط المراجع أثناء التصيير، إذ يمكن أن يؤدي ذلك إلى سلوك مفاجئ. عوض ذلك، قد ترغب في تعديل المراجع في معالجات الحدث والتأثيرات.

هل يجب أن استعمل متغير حالة واحد أم عدة متغيرات؟

إن كنت قادمًا من الأصناف، قد تميل إلى استدعاء useState()‎ مرةً واحدةً دومًا ووضع جميع الحالات في كائن واحد. لا شك أنَّك تستطيع فعل ذلك إن كنت ترغب في ذلك. إليك مثالٌ عن مكون يتبع حركة مؤشر الفأرة، إذ نبقي موضعه وحجمه في الحالة المحلية:

function Box() {
  const [state, setState] = useState({ left: 0, top: 0, width: 100, height: 100 });
  // ...
}

الآن، لنقل أننا نريد كتابة جزء من شيفرة تغيِّر left و top عندما يحرك المستخدم مؤشر الفأرة. لاحظ كيف يتوجب علينا دمج هذين الحقلين في كائن الحالة السابقة يدويًّا:

  // ...
  useEffect(() => {
    function handleWindowMouseMove(e) {
      // عدم فقدان الارتفاع والعرض "...state" نتأكد بنشر
      setState(state => ({ ...state, left: e.pageX, top: e.pageY }));
    }
    // ملاحظة: هذا لتنفيذ بسيط جدًا
    window.addEventListener('mousemove', handleWindowMouseMove);
    return () => window.removeEventListener('mousemove', handleWindowMouseMove);
  }, []);
  // ...

هذا بسببب أنَّه استبدلنا قيمة متغير حالة عندما حدثناها. هذا الأمر مختلف عن this.setState في الأصناف التي تدمج الحقول المحدَّثة. إذا فاتتك عملية الدمج التلقائي، يمكنك كتابة خطاف useLegacyState مخصص يدمج تحديثات حالة الكائن. على أي حال، نوصي بدلًا من ذلك بتقسيم الحالة إلى متغيرات حالة متعددة اعتمادًا على القيم التي تتغير سويةً. على سبيل المثال، يمكنا تقسيم حالة المكون الخاص بنا إلى الكائنين position و size، واستبدال position دون الحاجة للدمج:

function Box() {
  const [position, setPosition] = useState({ left: 0, top: 0 });
  const [size, setSize] = useState({ width: 100, height: 100 });

  useEffect(() => {
    function handleWindowMouseMove(e) {
      setPosition({ left: e.pageX, top: e.pageY });
    }
    // ...

فصل متيغرات حالة مستقلة له فائدةٌ أخرى هي تسهيل استخراج جزء من مترابط من الشيفرة إلى خطاف مخصص لاحقًا مثل:

function Box() {
  const position = useWindowPosition();
  const [size, setSize] = useState({ width: 100, height: 100 });
  // ...
}

function useWindowPosition() {
  const [position, setPosition] = useState({ left: 0, top: 0 });
  useEffect(() => {
    // ...
  }, []);
  return position;
}

لاحظ كيف كان بإمكاننا نقل الاستدعاء useState من متغير الحالة position والتأثير المرتبط به إلى خطاف مخصص دون تغيير الشيفرة. إن كانت جميع متغيرات الحالة في كائن واحد، فسيَصعُب استخراجها.

وضع جميع متغيرات الحالة في استدعاء واحد للخطاف useState، واستدعاء useState بكل حقل أمران يمكن تطبيقهما بشكل صحيح. تميل المكونات لتكون أكثر قابلية للقراءة عند تحقيق التوازن بين هذين النقيضين، وتجميع كل ما يرتبط بالحالة في بضعة متغيرات حالة مستقلة. إن أصبحت شيفرة الحالة معقدة، نوصي بإدارتها باستعمال مخفض أو خطاف مخصص.

هل يمكنني تنفيذ تأثير عند إجراء التحديثات فقط؟

تعدُّ هذه الحالة نادرة الاستعمال. إن احتجت إليها، يمكنك استعمال مرجعٍ قابل للتعديل لتخزين قيمة منطقية يدويًّا تشير إمَّا إلى كونك في أول عملية تصيير أو في عملية تصيير تالية ثم التحقق من هذه الراية في التأثير الخاص بك. (إن وجدت نفسك معتادًا على فعل هذا الأمر، يمكنك أن تنشئ خطافًا مخصصًا لذلك.)

كيف يمكن جلب الخاصية أو الحالة السابقة؟

في الوقت الحالي، يمكنك فعل ذلك يدويًّا باستعمال مرجع:

function Counter() {
  const [count, setCount] = useState(0);

  const prevCountRef = useRef();
  useEffect(() => {
    prevCountRef.current = count;
  });
  const prevCount = prevCountRef.current;

  return <h1>Now: {count}, before: {prevCount}</h1>;
}

قد تبدو هذه العملية معقدةً بعض الشيء ولكن يمكنك استخراجها إلى خطاف مخصص:

function Counter() {
  const [count, setCount] = useState(0);
  const prevCount = usePrevious(count);
  return <h1>Now: {count}, before: {prevCount}</h1>;
}

function usePrevious(value) {
  const ref = useRef();
  useEffect(() => {
    ref.current = value;
  });
  return ref.current;
}

لاحظ كيف سيعمل هذا من أجل الخاصيات، أو الحالات، أو أية قيمة أخرى محسوبة:

function Counter() {
  const [count, setCount] = useState(0);

  const calculation = count * 100;
  const prevCalculation = usePrevious(calculation);
  // ...

واردٌ في المستقبل أن توفر React خطافًا يدعى usePrevious مثلًا لفعل ذلك، إذ هذا السلوك شائع نسبيًا.

انظر أيضًا إلى السؤال التالي للاطلاع على النمط الموصى به من أجل الحالة المشتقة.

لماذا أرى خاصيات أو حالة قديمة في دالتي؟

أي دالة داخل مكون ما، بما في ذلك معالجات الأحداث والتأثيرات، "ترى" الخاصيات والحالة من التصيير الذي أُنشِئت فيه. انظر المثال التالي:

function Example() {
  const [count, setCount] = useState(0);

  function handleAlertClick() {
    setTimeout(() => {
      alert('You clicked on: ' + count);
    }, 3000);
  }

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        انقر على
      </button>
      <button onClick={handleAlertClick}>
        أظهر التنبيه
      </button>
    </div>
  );
}

إذا نقرت أولاً على "أظهر التنبيه" ثم قمت بزيادة العداد، فسيعرض التنبيه المتغير count في الوقت الذي نقرت فيه على زر "أظهر التنبيه". هذا يمنع الأخطاء التي تسببها الشيفرات البرمجية التي تفترض أنّ الخاصيات والحالة لا تتغير.

إذا كنت تريد عن قصد قراءة أحدث حالة من رد نداء غير متزامن، فيمكنك تخزينها في مرجع، ثمّ تعديلها وقرائتها.

أخيرًا، هناك سبب آخر محتمل لظهور خاصيات أو حالة قديمة، وهو إن كنت تستخدم تحسينا "لمصفوفة التبعية"، ولكنك لم تحدد جميع التبعيات بصورة صحيحة. على سبيل المثال، إذا حدّد تأثير ما المصفوفة الفارغة [] كوسيط ثان، بيد أنّه حاول قراءة خاصية ما بداخله، فسيظل "يرى" القيمة الأولية لتلك الخاصية. الحل هو إما إزالة مصفوفة التبعية، أو إصلاحها. إليك كيفية التعامل مع الدوال، وهذه استراتيجيات أخرى شائعة لتنفيذ التأثيرات بمعدل أقل دون تخطي التبعيات بشكل غير صحيح.

ملاحظة: لقد قدمنا قاعدة ESLint شاملة للتبعيات كجزء من حزمة eslint-plugin-React-hooks. والتي تطلق تحذيرا في حال تحديد التبعيات بصورة غير صحيحة وتقترح حلا لإصلاح الخلل.

كيف أنفِّذ getDerivedStateFromProps؟

رغم أنَّك لن تحتاج إليه على الأرجح، فيمكنك في حالات نادرة فعل ذلك (مثل تنفيذ المكون <Transition>) عبر تحديث الحالة بشكل صحيح أثناء عملية التصيير. ستعيد React تنفيذ المكون مع الحالة المحدَّثة مباشرةً بعد الخروج من أول عملية تصيير، لذا لن يؤثر ذلك على الأداء.

في الشيفرة التالية، نخزِّن القيمة السابقة للخاصية row في متغير حالة، ويمكننا بذلك إجراء عملية موازنة:

function ScrollView({row}) {
  let [isScrollingDown, setIsScrollingDown] = useState(false);
  let [prevRow, setPrevRow] = useState(null);

  if (row !== prevRow) {
    // isScrollingDown تغيرت منذ أخرى عملية تصيير. حدِّث  row الخاصية
    setIsScrollingDown(prevRow !== null && row > prevRow);
    setPrevRow(row);
  }

  return `Scrolling down: ${isScrollingDown}`;
}

قد يبدو ذلك غريبًا في البداية، ولكن إجراء تحديث أثناء التصيير هو ما يشبه سلوك getDerivedStateFromProps تمامًا من الناحية النظرية.

هل يوجد شيء يشبه forceUpdate؟

يحافظ الخطافان useState و useReducer كلاهما على الحالة عند إجراء تحديث عليها في حال كانت القيمة التالية هي نفس القيمة السابقة. تغيير الحالة يدويًّا (in place) واستدعاء الخطاف useState لن يؤدي إلى إعادة التصيير.

بشكل طبيعي، لا يجب عليك تعديل حالة محلية في React. على أي حال وكمخرج هروب، يمكنك استعمال عداد متزايد لإجبار إجراء إعادة الصيير حتى إن لم تتغير الحالة:

  const [ignored, forceUpdate] = useReducer(x => x + 1, 0);

  function handleClick() {
    forceUpdate();
  }

حاول تجنب هذه النمط قدر المستطاع.

أيمكنني إنشاء مرجع إلى مكون دالة؟

رغم أنّه لا يجب أن تحتاج إلى تنفيذ ذلك في أغلب الأحيان، قد تعرض بعض التوابع الأمرية على مكون أب (parent component) مع الخطاف useImperativeHandle.

كيف يمكنني قياس عقدة DOM؟

إحدى أبسط الطرق لقياس موضع أو حجم عقدة DOM هي استخدام ردود نداء المراجع. سوف تستدعي React رد النداء كلما تم ربط المرجع بعقدة مختلفة. إليك المثال التوضيحي التالي:

function MeasureExample() {
  const [height, setHeight] = useState(0);

  const measuredRef = useCallback(node => {
    if (node !== null) {
      setHeight(node.getBoundingClientRect().height);
    }
  }, []);

  return (
    <>
      <h1 ref={measuredRef}>Hello, world</h1>
      <h2>The above header is {Math.round(height)}px tall</h2>
    </>
  );
}

لم نختر useRef في هذا المثال لأن مرجع الكائن لا يُعلمنا بالتغييرات في قيمة المرجع الحالية. يضمن استخدام ردود نداء المراجع أنه حتى في حال قام أحد المكونات الفرعية بعرض العقدة المقاسة لاحقًا (على سبيل المثال استجابةً لنقرة) ، فسنتلقى إخطارًا بشأنها في المكون الأب ويمكننا تحديث القياسات.

لاحظ أننا مررنا [] كمصفوفة تبعية لاستخدامها في useCallback. هذا يضمن أن رد نداء المرجع الخاص بنا لن يتغير بين عمليات إعادة التصيير، وبالتالي فلن تستدعيها React دون داع.

في هذا المثال، لن يُستدعى رد نداء المرجع إلا عندما يوصل المكوّن أو يُفصل، نظرًا لأن المكوّن <h1> المُصيّر يظل موجودًا في جميع عمليات إعادة التصيير. إذا كنت تريد أن يتم إعلامك في كل مرة يتم فيه تغيير حجم أحد المكونات، فقد ترغب في استخدام ResizeObserverأو خطاف مبني عليها من طرف ثالث.

يمكنك استعمال هذا المثال في خطاف قابل لإعادة الاستخدام:

function MeasureExample() {
  const [rect, ref] = useClientRect();
  return (
    <>
      <h1 ref={ref}>Hello, world</h1>
      {rect !== null &&
        <h2>The above header is {Math.round(rect.height)}px tall</h2>
      }
    </>
  );
}

function useClientRect() {
  const [rect, setRect] = useState(null);
  const ref = useCallback(node => {
    if (node !== null) {
      setRect(node.getBoundingClientRect());
    }
  }, []);
  return [rect, ref];
}

ما الذي يعينه const [thing, setThing] = useState()‎؟

إن لم تكن هذه الصياغة مألوفة لديك، اطلع على الشرح المذكور في هذا القسم في توثيق خطاف الحالة.

تحسينات الأداء

أيمكنني تخطي تأثير ما في عمليات التحديث؟

نعم. اطلع على قسم "تنفيذ تأثير شرطيًّا". لاحظ أنَّ نسيان معالجة تحديثات يولد غالبًا أخطاء، إذ هذا هو سبب عدم كون هذا السلوك هو السلوك الافتراضي.

هل من الآمن حذف الدوال من قائمة التبعيات؟

بشكل عام، الجواب هو لا.

function Example({ someProp }) {
  function doSomething() {
    console.log(someProp);
  }

  useEffect(() => {
    doSomething();
  }, []); // 🔴 `someProp` والتي تستعمل `doSomething`  هذا ليس آمنا، إذ تستدعي
}

من الصعب تذكر الخاصيات أو الحالة التي تستخدمها الدوال خارج التأثير. لأجل هذا يجب التصريح بالدوال التي يحتاجها التأثير بداخله. سيسهّل ذلك عليك معرفة القيم من نطاق المكون الذي يعتمد عليه التأثير:

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

    doSomething();
  }, [someProp]); // ✅  `someProp` جيد، فالتأثير لا يستعمل إلا
}

إن لم تستخدم عقب هذا أي قيم من نطاق المكون، فمن الآمن إذن تمرير []:

useEffect(() => {
  function doSomething() {
    console.log('hello');
  }

  doSomething();
}, []); // ✅ لا بأس في هذه الحالة لأننا لم نستخدم أي قيم من نطاق المكون

اعتمادًا على حالة الاستخدام، أمامك عدة خيارات سنوضحها أدناه.

ملاحظة: لقد قدمنا قاعدة ESLint شاملة للتبعيات كجزء من حزمة eslint-plugin-React-hooks. والتي تطلق تحذيرا في حال تحديد التبعيات بصورة غير صحيحة وتقترح حلا لإصلاح الخلل.

دعونا نرى لماذا هذا مهم.

إذا مررت قائمة من التبعيات كوسيط أخير للدوال useEffect أو useLayoutEffect أو useMemo أو useCallback أو useImperativeHandle ، فيجب أن تتضمن جميع القيم المستخدمة داخل رد النداء والمشاركة في تدفق بيانات React. ويشمل ذلك الخاصيات والحالة وأي شيء مشتق منهما.

ليس من الآمن حذف دالة من قائمة التبعيات إلا إن كنت متأكدا أنه لا شيء فيها (أو الدوال المُستدعاة من قبلها) يشير إلى خاصيات أو حالة أو قيم مشتقة منهما. المثال التالي به خلل:

function ProductPage({ productId }) {
  const [product, setProduct] = useState(null);

  async function fetchProduct() {
    const response = await fetch('http://myapi/product/' + productId); // Uses productId prop
    const json = await response.json();
    setProduct(json);
  }

  useEffect(() => {
    fetchProduct();
  }, []); // 🔴 `productId` تستعمل `fetchProduct` غير صالح، لأنّ
  // ...
}

أفضل طريقة لإصلاح هذا الخلل هي نقل الدالة إلى داخل تأثيرك. هذا يسهل معرفة الخاصيات أو الحالة التي يستخدمها التأثير، والتأكد من أنّه تم التصريح بها جميعًا:

function ProductPage({ productId }) {
  const [product, setProduct] = useState(null);

  useEffect(() => {
    // عبر نقل هذه الدالة إلى داخل التأثير، يمكننا رؤية القيم التي يستخدمها
    async function fetchProduct() {
      const response = await fetch('http://myapi/product/' + productId);
      const json = await response.json();
      setProduct(json);
    }

    fetchProduct();
  }, [productId]); // ✅ productId صالح، لأن التأثير لا يستخدم إلا
  // ...
}

يتيح لك هذا أيضًا التعامل مع الاستجابات غير المتوقعة باستخدام متغير محلي داخل التأثير:

useEffect(() => {
    let ignore = false;
    async function fetchProduct() {
      const response = await fetch('http://myapi/product/' + productId);
      const json = await response.json();
      if (!ignore) setProduct(json);
    }

    fetchProduct();
    return () => { ignore = true };
  }, [productId]);

لقد نقلنا الدالة إلى داخل التأثير بحيث لا يلزم أن تكون في قائمة التبعيات الخاصة به.

ملاحظة: راجع هذا المثال التوضيحي وهذه المقالة لمعرفة المزيد عن كيفية جلب البيانات باستخدام الخطافات.

إذا لم تتمكن لسبب ما من نقل الدالة إلى داخل التأثير، فهناك بعض الخيارات المتاحة:

  • يمكنك محاولة نقل تلك الدالة إلى خارج المكون. في هذه الحالة، لن يشير المكون إلى أي خاصيات أو حالة، ولا يلزم أيضًا أن يكون في قائمة التبعيات.
  • إذا كانت الدالة التي تستدعيها محصورة على إجراء عمليات حسابية وكان استدعاؤها آمنا أثناء التصيير، فيمكنك استدعاؤها خارج التأثير بدلاً من ذلك، وجعل التأثير يعتمد على القيمة المُعادة.
  • كحل أخير، يمكنك إضافة الدالة إلى تبعيات التأثير ولكن مع تغليف تعريفها في خطاف useCallback. يضمن ذلك ألا تتغير عند كل تصيير إلا إن تغيرت تبعياتها أيضا:
function ProductPage({ productId }) {
  // ✅ لتجنب التغيير عن كل تصيير useCallback غلف باستخدام
  const fetchProduct = useCallback(() => {
    // ... Does something with productId ...
  }, [productId]); // ✅ useCallback تم تحديد جميع تبعيات

  return <ProductDetails fetchProduct={fetchProduct} />;
}

function ProductDetails({ fetchProduct }) {
  useEffect(() => {
    fetchProduct();
  }, [fetchProduct]); // ✅useCallback تم تحديد جميع تبعيات

  // ...
}

لاحظ أنه في المثال أعلاه، احتجنا إلى إبقاء الدالة في قائمة التبعيات. يضمن ذلك أن يؤدي التغيير في خاصية productId الخاصة بـ ProductPage تلقائيًا إلى بدء عملية إعادة الجلب في المكون ProductDetails.

ما الذي علي فعله إذا كانت تبعيات التأثير تتغير كثيرًا؟

في بعض الأحيان، قد يستخدم التأثير حالة تتغير كثيرًا. قد ترغب في حذف تلك الحالة من قائمة التبعيات، ولكن ذلك قد يؤدي إلى أخطاء:

function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const id = setInterval(() => {
      setCount(count + 1); // `count` يعتمد هذا التأثير على حالة
    }, 1000);
    return () => clearInterval(id);
  }, []); // 🔴 في التبعيات `count` هناك خلل، إذ لم يتم تحديد

  return <h1>{count}</h1>;
}

مجموعة التبعيات الفارغة [] تعني أنّ التأثير لن يُنفّذ إلا مرة واحدة عندما يتم وصل المكون، وليس عند كل إعادة تصيير. تكمن المشكلة في أنه داخل رد نداء setInterval، لا تتغير قيمة count، لأننا أنشأنا تعبيرا مغلقا (closure) مع ضبط قيمة count عند 0 كما كان عند تنفيذ رد نداء التأثير. في كل ثانية، يستدعي رد النداء setCount (0 + 1)‎ ، لذلك لا يتجاوز count القيمة1 أبدًا. سيؤدي تمرير [count] كقائمة للتبعيات إلى إصلاح الخلل، ولكنه سيؤدي إلى إعادة ضبط المجال عند كل تغيير. عمليا، سيُنفذ كل setInterval مرة واحدة قبل أن يُمحى (على غرار setTimeout.) قد لا يكون ذلك مرغوبًا. لإصلاح ذلك يمكننا استخدام التحديث الدالي لـ setState. والذي يتيح لنا تحديد كيفية تغيير الحالة دون الرجوع إلى الحالة الراهنة:

function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const id = setInterval(() => {
      setCount(c => c + 1); // ✅ `count` لا يتعلق هذا بالمتغير
    }, 1000);
    return () => clearInterval(id);
  }, []); // ✅  لا يستخدم التأثير أي متغير في نطاق المكون

  return <h1>{count}</h1>;
}

(هوية الدالة setCount ستبقى ثابتة، لذا من الآمن حذفها.)

يُنفّذ رد النداء setInterval مرة واحدة كل ثانية، ولكن في كل مرة يمكن للاستدعاء الداخلي لـ setCount استخدام قيمة مُحدثة لـ count  (تسمى c في رد النداء هنا.)

في الحالات المعقدة (مثلا، إن كانت حالة ما تعتمد على حالة أخرى)، حاول نقل منطق تحديث الحالة خارج التأثير باستخدام الخطاف useReducer. تقدم هذه المقالة مثالاً عن كيفية القيام بذلك. تبقى هوية الدالة dispatch  من useReducer ثابتة دائمًا - حتى لو تم التصريح عن دالة الاختزال (reducer) داخل المكون، مع قراءة خاصياته.

كملاذ أخير، إذا أردت استخدام شيء يشبه this  في الأصناف، يمكنك استخدام مرجع لتخزين متغير قابل للتغيير. ثم يمكنك كتابته أو قرائته كما يبين المثال التالي:

function Example(props) {
  //  اخفظ آحدث الخاصيات في المرجع
  const latestProps = useRef(props);
  useEffect(() => {
    latestProps.current = props;
  });

  useEffect(() => {
    function tick() {
      //  اقرأ أحدث خاصية في أي وقت
      console.log(latestProps.current);
    }

    const id = setInterval(tick, 1000);
    return () => clearInterval(id);
  }, []); // لا يعاد تنفيذ هذا التأثير أبدا
}

لا تفعل هذا إلا إن لم يكن لديك بديل أفضل، ذلك أن الاعتماد على العناصر القابلة للتغيير يصعّب التنبؤ بسلوك المكونات. إذا صادفت نمطا غامضا، فقم بتقديم التماس مرفوقا بمثال قابل للتنفيذ، وسنحاول المساعدة.

كيف يمكنني تنفيذ shouldComponentUpdate؟

يمكنك تغليف مكون دالة مع React.memo لموازنة خاصياته بشكل سطحي:

const Button = React.memo((props) => {
  // المكون الخاص بك
});

هذا ليس خطافًا لأنَّه لم يُنشَأ بالشكل الذي تُنشَأ فيه الخطافات. إنَّ React.memo يكافئ PureComponent ولكن يوازن الخاصيات فقط. (يمكنك أيضًا أن تضيف وسيطًا آخر لتحديد دالة موازنة مخصصة تأخذ الخاصيات القديمة والجديدة. إن أعادت القيمة true، فسيُتخطَّى التحديث.)

لا يوازن React.memo الحالة لعدم وجود كائن حالة وحيد لموازنته. مع ذلك، يمكنك جعل الأبناء في حالة نقية (pure) أيضًا أو حتى تحسين ابنٍ واحدٍ مع useMemo.

كيف يمكن استظهار (memoize) العمليات الحسابية؟

يمكِّنك الخطاف useMemo من تخزين الحسابات بين عدة عمليات تصيير عبر "تذكر" القيمة التي جرى حسابها مسبقًا:

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

تستدعي هذه الشيفرة computeExpensiveValue(a, b)‎؛ ولكن إن لم تتغير المدخلات [a, b] منذ آخر قيمة، سيتخطى الخطاف useMemo استدعاءها في المرة التالية ويعيد استعمال آخر قيمة أعادتها تلك الدالة.

تذكر أنَّ الدالة المُمرَّرة إلى useMemo تُنفَّذ أثناء عملية التصيير. لا تفعل أي شيء في هذه الأثناء لم تكن لتفعله بشكل طبيعي خلال عملية التصيير. على سبيل المثال، التأُثيرات الجانبية تتبع للخطاف useEffect وليس للخطاف useMemo.

يمكنك الاعتماد على الخطاف useMemo لتحسين الأداء، وليس لضمان الدلالات (semantic guarantee). في المستقبل، قد تختار React بأن "تنسى" بعض القيم المُستظهَرة (المحفوظة) وتعيد حسابها من جديد في عملية التصيير التالية وذلك لتحرير الذاكرة لمكونات غير ظاهرة على الشاشة (offscreen) مثلًا. اكتب أولًا شيفرتك لتعمل بشكل صحيح دون الخطاف useMemo، ومن ثمَّ أضفه لتحسين الأداء. (في حالات نادرة عندما لا يجب حساب قيمة مطلقًا، يمكنك حينئذٍ تهيئة مرجع بشكل كسول.)

أضف إلى ذلك أنَّ الخطاف useMemo يمكِّنك بسهولة من تخطي عملية إعادة تصيير لابن تستنزف الأداء:

function Parent({ a, b }) {
  // فقط `a` أعد عملية التصيير إن تغير:
  const child1 = useMemo(() => <Child1 a={a} />, [a]);
  // فقط `b` أعد عملية التصيير إن تغير:
  const child2 = useMemo(() => <Child2 b={b} />, [b]);
  return (
    <>
      {child1}
      {child2}
    </>
  )
}

لاحظ أنَّ هذا الأسلوك لن يعمل في حلقة تكرار لأنَّه لا يمكن أن توضع استدعاءات الخطافات داخل حلقة تكرار. على أي حال، تستطيع استخراج مكون منفصل لعنصر القائمة واستدعاء useMemo هنالك.

كيف يمكن إنشاء كائنات مستنزفة للأداء بشكل كسول؟

يمكِّنك الخطاف useMemo من استظهار (memoize) عملية حسابية مستهلكة للأداء إن بقيت المدخلات نفسها دون تغيُّر. مع ذلك، يعدُّ هذا بمثابة تلميح فقط، ولا يوجد أي شيء يضمن عدم تكرار تنفيذ العملية الحسابية. على أية حال، تحتاج أحيانًا إلى التأكد من إنشاء كائنٍ مرةً واحدةً فقط.

أول حالة استعمال شائعة هي عند استهلاك عملية إنشاء الحالة الأولية للأداء بشكل كبير:

function Table(props) {
  // ⚠️ في كل عملية تصيير createRows() يستدعى
  const [rows, setRows] = useState(createRows(props.count));
  // ...
}

لتجنب إعادة إنشاء الحالة الأولية المهملة، يمكننا تمرير دالة إلى useState:

function Table(props) {
  // ✅ مرة واحدة فقط createRows() يستدعى
  const [rows, setRows] = useState(() => createRows(props.count));
  // ...
}

ستستدعي React هذه الدالة خلال عملية أول عملية تصيير. لمزيد من التفاصيل، ارجع إلى توثيق الواجهة البرمجية للخطاف useState. في بعض الأحيان، ربما ترغب بتجنب إعادة إنشاء الحالة الأولية للخطاف useRef()‎. على سبيل المثال، ربما تريد التأكد من إنشاء بعض نسخ الأصناف الأمرية (imperative class instance) مرةً واحدةً فقط:

function Image(props) {
  // ⚠️ في كل عملية تصيير IntersectionObserver يُنشَأ
  const ref = useRef(new IntersectionObserver(onIntersect));
  // ...
}

لا يقبل الخطاف useRef دالة مخصصة إضافية مثل الخطاف useState. عوض ذلك، تستطيع كتابة دالة مخصصة تُنشئها وتضبطها بشكل كسول:

function Image(props) {
  const ref = useRef(null);

  // ✅ ٍمرةً واحدةً بِكَسَل IntersectionObserver يُنشَأ
  function getObserver() {
    let observer = ref.current;
    if (observer !== null) {
      return observer;
    }
    let newObserver = new IntersectionObserver(onIntersect);
    ref.current = newObserver;
    return newObserver;
  }

  // عندما تحتاج إليه getObserver() استدعي
  // ...
}

بذلك، تتجنب إنشاء كائنات مستهلكة للأداء حتى الحاجة الماسة إليها للمرة الأولى. إن كنت تستعمل Flow أو TypeScript، تستطيع أيضًا أن تعطي getObserver()‎ نوعًا غير معدوم (non-nullable type) للسهولة.

هل تتسم الخطافات بالبطئ لإنشائها دوالًا في عملية التصيير؟

لا. في المتصفحات الحديثة، لا يختلف الأداء الصافي (raw performance) للمغلفات (closures) بموازنته مع الأصناف اختلافًا كبيرًا باستثناء الحالات المبالغ بها.

إضافةً لذلك، يعدُّ تصميم الخطافات أكثر فعالية من ناحيتين هما:

  • تخلصت الخطافات من الكثير من الأعباء التي تطلبها الأصناف مثل عبء طلب إنشاء نُسخٍ للصنف وربط معالجات حدث بالباني.
  • لا تحتاج الشيفرة الاصطلاحية (Idiomatic code) التي تستعمل الخطافات إلى التشعب العميق لشجرة المكونات (deep component tree nesting) السائد في قواعد الشيفرة (codebases) التي تستعمل المكوانات ذات الترتيب الأعلى، وخاصييات التصيير، والسياق. مع شجرة مكونات صغيرة، يكون لدى React القليل من العمل لإنجازه.

تقليديًّا، المخاوف التي تدور حول الدوال السطرية (inline functions) وتأثيرها على الأداء في React تتعلق بكيفية تمرير ردود النداء الجديدة في كل تصيير يفصل تحسينات shouldComponentUpdate في المكونات الأبناء. تتعامل الخطافات مع هذه المشكلة من ثلاث نواحٍ هي:

  • يمكِّنك الخطاف useCallback من الإبقاء على مرجع رد النداء نفسه بين عمليات إعادة التصيير، لذا يستمر shouldComponentUpdate بالعمل:
// `b` أو `a` لن يتغير إلا إذا تغير
const memoizedCallback = useCallback(() => {
  doSomething(a, b);
}, [a, b]);
  • يجعل الخطاف useMemo عملية التحكم سهلةً عندما يَجرِي تحديث ابنٍ واحدٍ، مما يقلل من الحاجة إلى مكونات نقية (pure components).
  • أخيرًا، يقلل الخطاف useReducer الحاجة إلى تمرير ردود نداء عميقة كما سيُشرَح ذلك في الأسفل.

ما هو السبيل لتجنب تمرير ردود النداء للداخل؟

وجدنا أنَّ أغلب الأشخاص لا يحبون تمرير ردود النداء يدويًا عبر كل مستوى من شجرة المكون. رغم أنَّ العملية واضحة إلا أنَّها أشبه بعملية تركيب شبكة من الأنابيب المتشعبة (أنابيب شبكة المياه والصرف مثلًا).

في شجرات المكون الكبيرة، البديل الذي نوصي به هو تمرير الدالة dispatch للداخل (down) من useReducer عبر السياق:

const TodosDispatch = React.createContext(null);

function TodosApp() {
  // لن تتغير بين عمليات إعادة التصيير `dispatch` الدالة
  const [todos, dispatch] = useReducer(todosReducer);

  return (
    <TodosDispatch.Provider value={dispatch}>
      <DeepTree todos={todos} />
    </TodosDispatch.Provider>
  );
}

أي ابن في الشجرة داخل TodosApp يمكن أن يستخدم الدالة dispatch لتمرير أحداث (actions) للأعلى حتى تصل إلى TodosApp:

function DeepChild(props) {
  // من السياق dispatch إذا أردنا أن ننفذ حدثًا ما، يمكننا جلب
  const dispatch = useContext(TodosDispatch);

  function handleClick() {
    dispatch({ type: 'add', text: 'hello' });
  }

  return (
    <button onClick={handleClick}>Add todo</button>
  );
}

هذا مناسبٌ أكثر من ناحية الصيانة (لا حاجة للاستمرار بتوجيه ردود النداء)، ويجنبنا من الوقوع في مشكلة رد النداء (callback problem) كليًّا. تمرير dispatch للأسفل بالشكل الذي وضحناه هو النمط الذي ينصح باتباعه للتحديثات العميقة.

الجدير بالملاحظة أنَّه لا يزال بإمكانك اختيار إمَّا إن تمرِّر حالة التطبيق (application state) للأسفل مثل الخاصيات (الأكثر وضوحًا) أو مثل السياق (أكثر سهولةً للتحديثات العميقة جدًا). إن كنت تستعمل السياق لتمرير الحالة للأسفل أيضًا، استعمل نوعان مختلفان من السياق، إذ لا يتغير السياق dispatch مطلقًا؛ لذلك، لا تحتاج المكونات التي تقرأه للتصيير إلا إذا احتاجت أيضًا إلى حالة التطبيق.

كيف تجري عملية قراءة قيمة تتغير كثيرًا من الخطاف useCallback؟

ملاحظة: نوصي بتمرير dispatch للأسفل في السياق. بدلًا من ردود النداء الفردية في الخاصيات. الطريقة المتبعة في الأسفل مذكورة هنا فقط كتتمة وكمخرج هروب (escape hatch).

لاحظ أيضًا أن هذا النمط قد يسبب حدوث مشاكل في الوضع المتزامن (concurrent mode). نخطط لتوفير بدائل أفضل في المستقبل، ولكن الحل الأكثر أمانًا الأن هو إبطال رد النداء دومًا إن تغيرت بعض القيم التي يعتمد عليها.

في حالات نادرة، قد تحتاج إلى استظهار (memoize) رد نداء مع الخطاف useCallback ولكن عملية الاستظهار لا تعمل بشكل صحيح لأنَّ الدالة الداخلية يجب أن يعاد إنشاؤها في كثير من الأحيان. إن كانت الدالة التي تريد استظهارها هي معالج حدث ولا تستخدم أثناء التصيير، يمكنك استعمال مرجع كمتغير نسخة، وتخزين آخر قيمة محفوظة ضمنه يدويًا:

function Form() {
  const [text, updateText] = useState('');
  const textRef = useRef();

  useEffect(() => {
    textRef.current = text; // كتابتها إلى المرجع
  });

  const handleSubmit = useCallback(() => {
    const currentText = textRef.current; // قراءتها من المرجع
    alert(currentText);
  }, [textRef]); // [text] كما ستفعل handleSubmit لا تعيد إنشاء

  return (
    <>
      <input value={text} onChange={e => updateText(e.target.value)} />
      <ExpensiveTree onSubmit={handleSubmit} />
    </>
  );
}

هذا النمط معقدٌ كثيرًا ولكن يبدو أنَّك لا بأس من فعل ذلك كمخرج هروب لتحسين الأداء إن شعرت أنك بحاجة إليه. يصبح التعامل مع هذا النمط أسهل وأكثير تقبُّلًا إن استخرجته إلى خطاف مخصص:

function Form() {
  const [text, updateText] = useState('');
  //  `text` سيجري استظهاره حتى لو تغير:
  const handleSubmit = useEventCallback(() => {
    alert(text);
  }, [text]);

  return (
    <>
      <input value={text} onChange={e => updateText(e.target.value)} />
      <ExpensiveTree onSubmit={handleSubmit} />
    </>
  );
}

function useEventCallback(fn, dependencies) {
  const ref = useRef(() => {
    throw new Error('Cannot call an event handler while rendering.');
  });

  useEffect(() => {
    ref.current = fn;
  }, [fn, ...dependencies]);

  return useCallback(() => {
    const fn = ref.current;
    return fn();
  }, [ref]);
}

في كلا الحالتين، لا ننصح باستعمال هذا النمط، ودافع ذكره هنا هو للتكملة فقط. عوضًا عنه، يفضل تجنب تمرير ردود نداء بشكل عميق للأسفل.

ما خلف الستار

كيف تربط React استدعاءات الخطافات مع المكونات؟

تتعقب React المكونات قيد التصيير باستمرار. بفضل القواعد المخصصة بالخطافات، أصبحنا نعلم كيف تستدَعى تلك الخطافات من مكونات React (أو من الخطافات المخصصة التي تُستدعَى أيضًا من مكونات React).

هنالك قائمة داخلية لخلايا في الذاكرة مرتبطة مع كل مكون. هي عبارة عن كائنات JavaScript نستطيع أن نضع فيها بعض البيانات. عندما تستدعي خطافًا مثل useState()‎، يقرأ الخلية الحالية (أو يهيِّئها في أول عملية تصيير)، ثم يحرِّك المؤشر إلى الخلية التالية. هذا هو تفسير حصول كل استدعاء من استدعاءات الخطاف useState()‎ حالة محلية مستقلة.

ما هو المصدر الذي استقيت من الخطافات؟

الخطافات هي فكرة متراكبة ومستقاة من مصادر مختلفة منها:

ابتكر Sebastian Markbåge التصميم الأساسي للخطافات ثم أعيد تنقيح وصقله لاحقًا من قبل Andrew Clark، و Sophie Alpert، و Dominic Gannaway، وغيرهم من أعضاء فريق React.

 انظر أيضًا

مصادر