العربية

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

React's useDeferredValue: الدليل النهائي لأداء واجهة المستخدم غير المعيقة

في عالم تطوير الويب الحديث، تجربة المستخدم هي الأهم. لم تعد الواجهة السريعة والمستجيبة ترفًا، بل أصبحت توقعًا أساسيًا. بالنسبة للمستخدمين في جميع أنحاء العالم، على مجموعة واسعة من الأجهزة وظروف الشبكة، يمكن أن تكون واجهة المستخدم البطيئة والمتقطعة هي الفارق بين عميل عائد وعميل مفقود. وهنا يأتي دور ميزات React 18 المتزامنة، وتحديدًا خطاف useDeferredValue، لتغيير قواعد اللعبة.

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

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

فهم المشكلة الأساسية: واجهة المستخدم المعيقة

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

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

تنفيذ نموذجي (وبطيء)

إليك كيف قد يبدو الكود في عالم ما قبل React 18، أو بدون استخدام الميزات المتزامنة:

هيكل المكون:

ملف: SearchPage.js

import React, { useState } from 'react'; import ProductList from './ProductList'; import { generateProducts } from './data'; // a function that creates a large array const allProducts = generateProducts(20000); // Let's imagine 20,000 products function SearchPage() { const [query, setQuery] = useState(''); const filteredProducts = allProducts.filter(product => { return product.name.toLowerCase().includes(query.toLowerCase()); }); function handleChange(e) { setQuery(e.target.value); } return (

); } export default SearchPage;

لماذا هذا بطيء؟

دعنا نتتبع إجراء المستخدم:

  1. يكتب المستخدم حرفًا، لنقل 'a'.
  2. يتم إطلاق حدث onChange، مستدعيًا handleChange.
  3. يتم استدعاء setQuery('a'). هذا يجدول إعادة عرض لمكون SearchPage.
  4. يبدأ React في إعادة العرض.
  5. داخل عملية العرض، يتم تنفيذ السطر const filteredProducts = allProducts.filter(...). هذا هو الجزء المكلف. تصفية مصفوفة من 20,000 عنصر، حتى مع فحص 'includes' بسيط، تستغرق وقتًا.
  6. أثناء حدوث هذه التصفية، تكون السلسلة الرئيسية للمتصفح مشغولة تمامًا. لا يمكنها معالجة أي إدخال جديد من المستخدم، ولا يمكنها تحديث حقل الإدخال بصريًا، ولا يمكنها تشغيل أي JavaScript آخر. واجهة المستخدم معيقة.
  7. بمجرد الانتهاء من التصفية، يشرع React في عرض مكون ProductList، والذي قد يكون بحد ذاته عملية ثقيلة إذا كان يعرض آلاف عقد DOM.
  8. أخيرًا، بعد كل هذا العمل، يتم تحديث DOM. يرى المستخدم حرف 'a' يظهر في مربع الإدخال، ويتم تحديث القائمة.

إذا كان المستخدم يكتب بسرعة - لنقل "apple" - فإن هذه العملية المعيقة بأكملها تحدث لـ 'a'، ثم 'ap'، ثم 'app'، و'appl'، و'apple'. والنتيجة هي تأخر ملحوظ حيث يتقطع حقل الإدخال ويكافح لمواكبة كتابة المستخدم. هذه تجربة مستخدم سيئة، خاصة على الأجهزة الأقل قوة الشائعة في أجزاء كثيرة من العالم.

تقديم التزامن في React 18

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

مع التزامن، يمكن لـ React تصنيف التحديثات إلى نوعين:

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

ما هو `useDeferredValue`؟ شرح مفصل

في جوهره، useDeferredValue هو خطاف يتيح لك إخبار React بأن قيمة معينة في مكونك ليست عاجلة. يقبل قيمة ويعيد نسخة جديدة من تلك القيمة التي سوف "تتأخر" إذا كانت هناك تحديثات عاجلة تحدث.

الصيغة (Syntax)

الخطاف سهل الاستخدام بشكل لا يصدق:

import { useDeferredValue } from 'react'; const deferredValue = useDeferredValue(value);

هذا كل شيء. تمرر له قيمة، ويعطيك نسخة مؤجلة من تلك القيمة.

كيف يعمل من الداخل

دعنا نزيل الغموض عن السحر. عندما تستخدم useDeferredValue(query)، إليك ما يفعله React:

  1. العرض الأولي: في العرض الأول، ستكون قيمة deferredQuery هي نفسها القيمة الأولية لـ query.
  2. يحدث تحديث عاجل: يكتب المستخدم حرفًا جديدًا. تتحدث حالة query من 'a' إلى 'ap'.
  3. العرض ذو الأولوية العالية: يقوم React فورًا بتشغيل إعادة عرض. خلال عملية إعادة العرض العاجلة الأولى هذه، يعرف useDeferredValue أن هناك تحديثًا عاجلاً قيد التقدم. لذا، فإنه لا يزال يعيد القيمة السابقة، 'a'. يتم إعادة عرض مكونك بسرعة لأن قيمة حقل الإدخال تصبح 'ap' (من الحالة)، لكن الجزء من واجهة المستخدم الذي يعتمد على deferredQuery (القائمة البطيئة) لا يزال يستخدم القيمة القديمة ولا يحتاج إلى إعادة حسابه. تظل واجهة المستخدم مستجيبة.
  4. العرض ذو الأولوية المنخفضة: مباشرة بعد اكتمال العرض العاجل، يبدأ React عملية إعادة عرض ثانية غير عاجلة في الخلفية. في *هذا* العرض، يعيد useDeferredValue القيمة الجديدة، 'ap'. هذا العرض في الخلفية هو ما يطلق عملية التصفية المكلفة.
  5. قابلية المقاطعة: هذا هو الجزء الرئيسي. إذا كتب المستخدم حرفًا آخر ('app') بينما لا يزال العرض ذو الأولوية المنخفضة لـ 'ap' قيد التقدم، فسيتجاهل React ذلك العرض الخلفي ويبدأ من جديد. إنه يعطي الأولوية للتحديث العاجل الجديد ('app')، ثم يجدول عرضًا خلفيًا جديدًا بأحدث قيمة مؤجلة.

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

التنفيذ العملي: إصلاح بحثنا البطيء

دعنا نعيد هيكلة مثالنا السابق باستخدام useDeferredValue لرؤيته عمليًا.

ملف: SearchPage.js (محسن)

import React, { useState, useDeferredValue, useMemo } from 'react'; import ProductList from './ProductList'; import { generateProducts } from './data'; const allProducts = generateProducts(20000); // A component to display the list, memoized for performance const MemoizedProductList = React.memo(ProductList); function SearchPage() { const [query, setQuery] = useState(''); // 1. Defer the query value. This value will lag behind the 'query' state. const deferredQuery = useDeferredValue(query); // 2. The expensive filtering is now driven by the deferredQuery. // We also wrap this in useMemo for further optimization. const filteredProducts = useMemo(() => { console.log('Filtering for:', deferredQuery); return allProducts.filter(product => { return product.name.toLowerCase().includes(deferredQuery.toLowerCase()); }); }, [deferredQuery]); // Only re-calculates when deferredQuery changes function handleChange(e) { // This state update is urgent and will be processed immediately setQuery(e.target.value); } return (

{/* 3. The input is controlled by the high-priority 'query' state. It feels instant. */} {/* 4. The list is rendered using the result of the deferred, low-priority update. */}
); } export default SearchPage;

التحول في تجربة المستخدم

مع هذا التغيير البسيط، تتحول تجربة المستخدم:

يبدو التطبيق الآن أسرع وأكثر احترافية بشكل ملحوظ.

`useDeferredValue` مقابل `useTransition`: ما الفرق؟

هذه إحدى نقاط الالتباس الأكثر شيوعًا للمطورين الذين يتعلمون React المتزامن. يتم استخدام كل من useDeferredValue و useTransition لتمييز التحديثات على أنها غير عاجلة، لكن يتم تطبيقهما في مواقف مختلفة.

الفرق الرئيسي هو: أين تملك السيطرة؟

`useTransition`

تستخدم useTransition عندما يكون لديك سيطرة على الكود الذي يطلق تحديث الحالة. يمنحك دالة، تسمى عادةً startTransition، لتغليف تحديث حالتك بها.

const [isPending, startTransition] = useTransition(); function handleChange(e) { const nextValue = e.target.value; // Update the urgent part immediately setInputValue(nextValue); // Wrap the slow update in startTransition startTransition(() => { setSearchQuery(nextValue); }); }

`useDeferredValue`

تستخدم useDeferredValue عندما لا تتحكم في الكود الذي يحدث القيمة. يحدث هذا غالبًا عندما تأتي القيمة من الخصائص (props)، أو من مكون أب، أو من خطاف آخر توفره مكتبة خارجية.

function SlowList({ valueFromParent }) { // We don't control how valueFromParent is set. // We just receive it and want to defer rendering based on it. const deferredValue = useDeferredValue(valueFromParent); // ... use deferredValue to render the slow part of the component }

ملخص المقارنة

الميزة `useTransition` `useDeferredValue`
ما الذي يغلفه دالة تحديث الحالة (مثل startTransition(() => setState(...))) قيمة (مثل useDeferredValue(myValue))
نقطة التحكم عندما تتحكم في معالج الحدث أو مشغل التحديث. عندما تتلقى قيمة (مثلًا من الخصائص) وليس لديك سيطرة على مصدرها.
حالة التحميل يوفر قيمة منطقية مدمجة `isPending`. لا توجد علامة مدمجة، ولكن يمكن اشتقاقها باستخدام `const isStale = originalValue !== deferredValue;`.
تشبيه أنت المرسل، تقرر أي قطار (تحديث حالة) يغادر على المسار البطيء. أنت مدير محطة، ترى قيمة تصل بالقطار وتقرر الاحتفاظ بها في المحطة للحظة قبل عرضها على اللوحة الرئيسية.

حالات استخدام وأنماط متقدمة

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

النمط 1: إظهار واجهة مستخدم "قديمة" كرد فعل

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

يمكنك تحقيق ذلك بمقارنة القيمة الأصلية بالقيمة المؤجلة. إذا كانتا مختلفتين، فهذا يعني أن هناك عرضًا خلفيًا قيد الانتظار.

function SearchPage() { const [query, setQuery] = useState(''); const deferredQuery = useDeferredValue(query); // This boolean tells us if the list is lagging behind the input const isStale = query !== deferredQuery; const filteredProducts = useMemo(() => { // ... expensive filtering using deferredQuery }, [deferredQuery]); return (

setQuery(e.target.value)} />
); }

في هذا المثال، بمجرد أن يكتب المستخدم، تصبح isStale قيمتها true. تتلاشى القائمة قليلاً، مما يشير إلى أنها على وشك التحديث. بمجرد اكتمال العرض المؤجل، تصبح query و deferredQuery متساويتين مرة أخرى، وتصبح isStale قيمتها false، وتعود القائمة إلى التعتيم الكامل مع البيانات الجديدة. هذا هو المعادل لعلامة isPending من useTransition.

النمط 2: تأجيل التحديثات على الرسوم البيانية والتصورات

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

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

function ChartDashboard() { const [year, setYear] = useState(2023); const deferredYear = useDeferredValue(year); // HeavyChart is a memoized component that does expensive calculations // It will only re-render when the deferredYear value settles. const chartData = useMemo(() => computeChartData(deferredYear), [deferredYear]); return (

setYear(parseInt(e.target.value, 10))} /> Selected Year: {year}
); }

أفضل الممارسات والمزالق الشائعة

على الرغم من قوته، يجب استخدام useDeferredValue بحكمة. إليك بعض أفضل الممارسات الرئيسية التي يجب اتباعها:

التأثير على تجربة المستخدم العالمية (UX)

إن تبني أدوات مثل useDeferredValue ليس مجرد تحسين تقني؛ إنه التزام بتجربة مستخدم أفضل وأكثر شمولاً لجمهور عالمي.

الخاتمة

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

من خلال فهم المبادئ الأساسية للتزامن، ومعرفة متى تستخدم useDeferredValue مقابل useTransition، وتطبيق أفضل الممارسات مثل التخزين المؤقت (memoization) وتقديم ملاحظات للمستخدم، يمكنك القضاء على تقطع واجهة المستخدم وبناء تطبيقات ليست وظيفية فحسب، بل ممتعة في الاستخدام. في سوق عالمي تنافسي، يعد تقديم تجربة مستخدم سريعة وسريعة الاستجابة ومتاحة للجميع هو الميزة النهائية، وuseDeferredValue هي واحدة من أقوى الأدوات في ترسانتك لتحقيق ذلك.