تعلم كيفية استخدام خطاف useActionState في React بفعالية لتطبيق إلغاء الارتداد للحد من معدل الإجراءات، مما يحسن الأداء وتجربة المستخدم في التطبيقات التفاعلية.
React useActionState: تطبيق إلغاء الارتداد للحد الأمثل من معدل الإجراءات
في تطبيقات الويب الحديثة، تعد معالجة تفاعلات المستخدم بكفاءة أمرًا بالغ الأهمية. فالإجراءات مثل إرسال النماذج، واستعلامات البحث، وتحديثات البيانات غالبًا ما تطلق عمليات من جانب الخادم. ومع ذلك، يمكن أن تؤدي الاستدعاءات المفرطة للخادم، خاصة تلك التي يتم تشغيلها في تتابع سريع، إلى اختناقات في الأداء وتدهور تجربة المستخدم. وهنا يأتي دور إلغاء الارتداد (debouncing)، ويقدم خطاف useActionState في React حلاً قويًا وأنيقًا.
ما هو إلغاء الارتداد؟
إلغاء الارتداد هو ممارسة برمجية تستخدم لضمان عدم تشغيل المهام التي تستغرق وقتًا طويلاً بشكل متكرر، وذلك عن طريق تأخير تنفيذ دالة حتى بعد فترة معينة من عدم النشاط. فكر في الأمر على هذا النحو: تخيل أنك تبحث عن منتج على موقع للتجارة الإلكترونية. بدون إلغاء الارتداد، كل ضغطة مفتاح في شريط البحث ستطلق طلبًا جديدًا إلى الخادم لجلب نتائج البحث. قد يؤدي هذا إلى إرهاق الخادم وتقديم تجربة متقطعة وغير مستجيبة للمستخدم. مع إلغاء الارتداد، يتم إرسال طلب البحث فقط بعد أن يتوقف المستخدم عن الكتابة لفترة قصيرة (على سبيل المثال، 300 مللي ثانية).
لماذا نستخدم useActionState لإلغاء الارتداد؟
يقدم خطاف useActionState، الذي تم تقديمه في React 18، آلية لإدارة تحديثات الحالة غير المتزامنة الناتجة عن الإجراءات، خاصة داخل مكونات خادم React. وهو مفيد بشكل خاص مع إجراءات الخادم حيث يسمح لك بإدارة حالات التحميل والأخطاء مباشرة داخل المكون الخاص بك. عند إقرانه بتقنيات إلغاء الارتداد، يوفر useActionState طريقة نظيفة وعالية الأداء لإدارة تفاعلات الخادم التي يطلقها إدخال المستخدم. قبل useActionState، كان تنفيذ هذا النوع من الوظائف يتضمن غالبًا إدارة الحالة يدويًا باستخدام useState و useEffect، مما يؤدي إلى كود أكثر تفصيلاً وعرضة للخطأ.
تطبيق إلغاء الارتداد مع useActionState: دليل خطوة بخطوة
دعنا نستكشف مثالًا عمليًا لتطبيق إلغاء الارتداد باستخدام useActionState. سننظر في سيناريو يكتب فيه المستخدم في حقل إدخال، ونريد تحديث قاعدة بيانات من جانب الخادم بالنص المُدخَل، ولكن فقط بعد تأخير قصير.
الخطوة 1: إعداد المكون الأساسي
أولاً، سنقوم بإنشاء مكون وظيفي بسيط يحتوي على حقل إدخال:
import React, { useState, useCallback } from 'react';
import { useActionState } from 'react-dom/server';
async function updateDatabase(prevState: any, formData: FormData) {
// Simulate a database update
const text = formData.get('text') as string;
await new Promise(resolve => setTimeout(resolve, 500)); // Simulate network latency
return { success: true, message: `Updated with: ${text}` };
}
function MyComponent() {
const [debouncedText, setDebouncedText] = useState('');
const [state, dispatch] = useActionState(updateDatabase, {success: false, message: ""});
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const newText = event.target.value;
setDebouncedText(newText);
};
return (
<form action={dispatch}>
<input type="text" name="text" value={debouncedText} onChange={handleChange} />
<button type="submit">Update</button>
<p>{state.message}</p>
</form>
);
}
export default MyComponent;
في هذا الكود:
- نستورد الخطافات اللازمة:
useState،useCallback، وuseActionState. - نعرّف دالة غير متزامنة
updateDatabaseتحاكي تحديثًا من جانب الخادم. تأخذ هذه الدالة الحالة السابقة وبيانات النموذج كوسائط. - يتم تهيئة
useActionStateبالدالةupdateDatabaseوكائن حالة أولي. - تقوم الدالة
handleChangeبتحديث الحالة المحليةdebouncedTextبقيمة الإدخال.
الخطوة 2: تطبيق منطق إلغاء الارتداد
الآن، سندخل منطق إلغاء الارتداد. سنستخدم دالتي setTimeout و clearTimeout لتأخير استدعاء دالة dispatch التي يعيدها `useActionState`.
import React, { useState, useRef, useCallback } from 'react';
import { useActionState } from 'react-dom/server';
async function updateDatabase(prevState: any, formData: FormData) {
// Simulate a database update
const text = formData.get('text') as string;
await new Promise(resolve => setTimeout(resolve, 500)); // Simulate network latency
return { success: true, message: `Updated with: ${text}` };
}
function MyComponent() {
const [debouncedText, setDebouncedText] = useState('');
const [state, dispatch] = useActionState(updateDatabase, {success: false, message: ""});
const timeoutRef = useRef<number | null>(null);
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const newText = event.target.value;
setDebouncedText(newText);
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
timeoutRef.current = window.setTimeout(() => {
const formData = new FormData();
formData.append('text', newText);
dispatch(formData);
}, 300);
};
return (
<div>
<input type="text" value={debouncedText} onChange={handleChange} />
<p>{state.message}</p>
</div>
);
}
export default MyComponent;
إليكم ما تغير:
- أضفنا خطاف
useRefيسمىtimeoutRefلتخزين معرّف المهلة. هذا يسمح لنا بمسح المهلة إذا كتب المستخدم مرة أخرى قبل انتهاء فترة التأخير. - داخل
handleChange: - نقوم بمسح أي مهلة موجودة باستخدام
clearTimeoutإذا كان لـtimeoutRef.currentقيمة. - نقوم بتعيين مهلة جديدة باستخدام
setTimeout. ستنفذ هذه المهلة دالةdispatch(مع بيانات النموذج المحدثة) بعد 300 مللي ثانية من عدم النشاط. - نقلنا استدعاء dispatch من النموذج إلى الدالة التي تم إلغاء ارتدادها. نستخدم الآن عنصر إدخال قياسي بدلاً من نموذج، ونطلق إجراء الخادم برمجيًا.
الخطوة 3: التحسين للأداء وتسرب الذاكرة
التنفيذ السابق وظيفي، ولكن يمكن تحسينه أكثر لمنع تسرب الذاكرة المحتمل. إذا تم إلغاء تحميل المكون بينما لا تزال هناك مهلة معلقة، فستظل دالة رد نداء المهلة تُنفذ، مما قد يؤدي إلى أخطاء أو سلوك غير متوقع. يمكننا منع ذلك عن طريق مسح المهلة في خطاف useEffect عند إلغاء تحميل المكون:
import React, { useState, useRef, useCallback, useEffect } from 'react';
import { useActionState } from 'react-dom/server';
async function updateDatabase(prevState: any, formData: FormData) {
// Simulate a database update
const text = formData.get('text') as string;
await new Promise(resolve => setTimeout(resolve, 500)); // Simulate network latency
return { success: true, message: `Updated with: ${text}` };
}
function MyComponent() {
const [debouncedText, setDebouncedText] = useState('');
const [state, dispatch] = useActionState(updateDatabase, {success: false, message: ""});
const timeoutRef = useRef<number | null>(null);
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const newText = event.target.value;
setDebouncedText(newText);
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
timeoutRef.current = window.setTimeout(() => {
const formData = new FormData();
formData.append('text', newText);
dispatch(formData);
}, 300);
};
useEffect(() => {
return () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
};
}, []);
return (
<div>
<input type="text" value={debouncedText} onChange={handleChange} />
<p>{state.message}</p>
</div>
);
}
export default MyComponent;
أضفنا خطاف useEffect مع مصفوفة تبعية فارغة. هذا يضمن أن التأثير يعمل فقط عند تحميل المكون وإلغاء تحميله. داخل دالة التنظيف الخاصة بالتأثير (التي يعيدها التأثير)، نقوم بمسح المهلة إذا كانت موجودة. هذا يمنع تنفيذ دالة رد نداء المهلة بعد إلغاء تحميل المكون.
بديل: استخدام مكتبة إلغاء الارتداد
بينما يوضح التنفيذ أعلاه المفاهيم الأساسية لإلغاء الارتداد، فإن استخدام مكتبة مخصصة لإلغاء الارتداد يمكن أن يبسط الكود ويقلل من خطر الأخطاء. توفر مكتبات مثل lodash.debounce تطبيقات قوية ومختبرة جيدًا لإلغاء الارتداد.
إليك كيفية استخدام lodash.debounce مع useActionState:
import React, { useState, useCallback, useEffect } from 'react';
import { useActionState } from 'react-dom/server';
import debounce from 'lodash.debounce';
async function updateDatabase(prevState: any, formData: FormData) {
// Simulate a database update
const text = formData.get('text') as string;
await new Promise(resolve => setTimeout(resolve, 500)); // Simulate network latency
return { success: true, message: `Updated with: ${text}` };
}
function MyComponent() {
const [debouncedText, setDebouncedText] = useState('');
const [state, dispatch] = useActionState(updateDatabase, {success: false, message: ""});
const debouncedDispatch = useCallback(debounce((text: string) => {
const formData = new FormData();
formData.append('text', text);
dispatch(formData);
}, 300), [dispatch]);
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const newText = event.target.value;
setDebouncedText(newText);
debouncedDispatch(newText);
};
return (
<div>
<input type="text" value={debouncedText} onChange={handleChange} />
<p>{state.message}</p>
</div>
);
}
export default MyComponent;
في هذا المثال:
- نستورد دالة
debounceمنlodash.debounce. - نقوم بإنشاء نسخة مُلغاة الارتداد من دالة
dispatchباستخدامuseCallbackوdebounce. يضمن خطافuseCallbackأن يتم إنشاء الدالة مُلغاة الارتداد مرة واحدة فقط، وتتضمن مصفوفة التبعيةdispatchلضمان تحديث الدالة إذا تغيرت دالةdispatch. - في دالة
handleChange، نقوم ببساطة باستدعاء دالةdebouncedDispatchمع النص الجديد.
اعتبارات عالمية وأفضل الممارسات
عند تطبيق إلغاء الارتداد، خاصة في التطبيقات ذات الجمهور العالمي، ضع في اعتبارك ما يلي:
- كمون الشبكة: يمكن أن يختلف كمون الشبكة بشكل كبير اعتمادًا على موقع المستخدم وظروف الشبكة. قد يكون تأخير إلغاء الارتداد الذي يعمل بشكل جيد للمستخدمين في منطقة ما قصيرًا جدًا أو طويلاً جدًا للمستخدمين في منطقة أخرى. ضع في اعتبارك السماح للمستخدمين بتخصيص تأخير إلغاء الارتداد أو تعديل التأخير ديناميكيًا بناءً على ظروف الشبكة. هذا مهم بشكل خاص للتطبيقات المستخدمة في مناطق ذات وصول غير موثوق بالإنترنت، مثل أجزاء من إفريقيا أو جنوب شرق آسيا.
- محررات أسلوب الإدخال (IMEs): يستخدم المستخدمون في العديد من البلدان الآسيوية IMEs لإدخال النص. غالبًا ما تتطلب هذه المحررات ضغطات مفاتيح متعددة لتكوين حرف واحد. إذا كان تأخير إلغاء الارتداد قصيرًا جدًا، فقد يتعارض مع عملية IME، مما يؤدي إلى تجربة مستخدم محبطة. ضع في اعتبارك زيادة تأخير إلغاء الارتداد للمستخدمين الذين يستخدمون IMEs، أو استخدم مستمع حدث أكثر ملاءمة لتكوين IME.
- إمكانية الوصول: يمكن أن يؤثر إلغاء الارتداد على إمكانية الوصول، خاصة للمستخدمين الذين يعانون من إعاقات حركية. تأكد من أن تأخير إلغاء الارتداد ليس طويلاً جدًا، وقدم طرقًا بديلة للمستخدمين لتشغيل الإجراء إذا لزم الأمر. على سبيل المثال، يمكنك توفير زر إرسال يمكن للمستخدمين النقر عليه لتشغيل الإجراء يدويًا.
- حمل الخادم: يساعد إلغاء الارتداد في تقليل حمل الخادم، ولكن لا يزال من المهم تحسين كود جانب الخادم لمعالجة الطلبات بكفاءة. استخدم التخزين المؤقت، وفهرسة قاعدة البيانات، وتقنيات تحسين الأداء الأخرى لتقليل الحمل على الخادم.
- معالجة الأخطاء: قم بتنفيذ معالجة أخطاء قوية للتعامل بأمان مع أي أخطاء تحدث أثناء عملية التحديث من جانب الخادم. اعرض رسائل خطأ مفيدة للمستخدم، وقدم خيارات لإعادة محاولة الإجراء.
- ملاحظات المستخدم: قدم ملاحظات مرئية واضحة للمستخدم للإشارة إلى أنه يتم معالجة مدخلاتهم. قد يشمل ذلك مؤشر تحميل دوار، أو شريط تقدم، أو رسالة بسيطة مثل "جارٍ التحديث...". بدون ملاحظات واضحة، قد يصاب المستخدمون بالارتباك أو الإحباط، خاصة إذا كان تأخير إلغاء الارتداد طويلاً نسبيًا.
- التوطين (Localization): تأكد من أن جميع النصوص والرسائل مترجمة بشكل صحيح لمختلف اللغات والمناطق. يشمل ذلك رسائل الخطأ، ومؤشرات التحميل، وأي نص آخر يتم عرضه للمستخدم.
مثال: إلغاء ارتداد شريط البحث
دعنا نأخذ مثالًا أكثر واقعية: شريط بحث في تطبيق للتجارة الإلكترونية. نريد إلغاء ارتداد استعلام البحث لتجنب إرسال عدد كبير جدًا من الطلبات إلى الخادم أثناء كتابة المستخدم.
import React, { useState, useCallback, useEffect } from 'react';
import { useActionState } from 'react-dom/server';
import debounce from 'lodash.debounce';
async function searchProducts(prevState: any, formData: FormData) {
// Simulate a product search
const query = formData.get('query') as string;
await new Promise(resolve => setTimeout(resolve, 500)); // Simulate network latency
// In a real application, you would fetch search results from a database or API here
const results = [`Product A matching "${query}"`, `Product B matching "${query}"`];
return { success: true, message: `Search results for: ${query}`, results: results };
}
function SearchBar() {
const [searchQuery, setSearchQuery] = useState('');
const [state, dispatch] = useActionState(searchProducts, {success: false, message: "", results: []});
const [searchResults, setSearchResults] = useState<string[]>([]);
const debouncedSearch = useCallback(debounce((query: string) => {
const formData = new FormData();
formData.append('query', query);
dispatch(formData);
}, 300), [dispatch]);
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const newQuery = event.target.value;
setSearchQuery(newQuery);
debouncedSearch(newQuery);
};
useEffect(() => {
if(state.success){
setSearchResults(state.results);
}
}, [state]);
return (
<div>
<input type="text" placeholder="Search for products..." value={searchQuery} onChange={handleChange} />
<p>{state.message}</p>
<ul>
{searchResults.map((result, index) => (
<li key={index}>{result}</li>
))}
</ul>
</div>
);
}
export default SearchBar;
يوضح هذا المثال كيفية إلغاء ارتداد استعلام بحث باستخدام lodash.debounce و useActionState. تحاكي دالة searchProducts بحثًا عن منتج، ويعرض مكون SearchBar نتائج البحث. في تطبيق واقعي، ستقوم دالة searchProducts بجلب نتائج البحث من واجهة برمجة تطبيقات خلفية.
ما بعد إلغاء الارتداد الأساسي: تقنيات متقدمة
بينما توضح الأمثلة أعلاه إلغاء الارتداد الأساسي، هناك تقنيات أكثر تقدمًا يمكن استخدامها لتحسين الأداء وتجربة المستخدم بشكل أكبر:
- إلغاء الارتداد عند الحافة الأمامية (Leading Edge Debouncing): مع إلغاء الارتداد القياسي، يتم تنفيذ الدالة بعد التأخير. مع إلغاء الارتداد عند الحافة الأمامية، يتم تنفيذ الدالة في بداية التأخير، ويتم تجاهل الاستدعاءات اللاحقة أثناء التأخير. يمكن أن يكون هذا مفيدًا في السيناريوهات التي تريد فيها تقديم ملاحظات فورية للمستخدم.
- إلغاء الارتداد عند الحافة الخلفية (Trailing Edge Debouncing): هذه هي تقنية إلغاء الارتداد القياسية، حيث يتم تنفيذ الدالة بعد التأخير.
- الخنق (Throttling): يشبه الخنق إلغاء الارتداد، ولكن بدلاً من تأخير تنفيذ الدالة حتى بعد فترة من عدم النشاط، يحد الخنق من المعدل الذي يمكن به استدعاء الدالة. على سبيل المثال، يمكنك خنق دالة ليتم استدعاؤها مرة واحدة على الأكثر كل 100 مللي ثانية.
- إلغاء الارتداد التكيفي (Adaptive Debouncing): يقوم إلغاء الارتداد التكيفي بضبط تأخير إلغاء الارتداد ديناميكيًا بناءً على سلوك المستخدم أو ظروف الشبكة. على سبيل المثال، يمكنك تقليل تأخير إلغاء الارتداد إذا كان المستخدم يكتب ببطء شديد، أو زيادة التأخير إذا كان كمون الشبكة مرتفعًا.
الخاتمة
يعد إلغاء الارتداد تقنية حاسمة لتحسين أداء وتجربة المستخدم في تطبيقات الويب التفاعلية. يوفر خطاف useActionState في React طريقة قوية وأنيقة لتطبيق إلغاء الارتداد، خاصة بالاقتران مع مكونات خادم React وإجراءات الخادم. من خلال فهم مبادئ إلغاء الارتداد وقدرات useActionState، يمكن للمطورين بناء تطبيقات سريعة الاستجابة وفعالة وسهلة الاستخدام تتوسع عالميًا. تذكر أن تأخذ في الاعتبار عوامل مثل كمون الشبكة، واستخدام IME، وإمكانية الوصول عند تطبيق إلغاء الارتداد في التطبيقات ذات الجمهور العالمي. اختر تقنية إلغاء الارتداد المناسبة (الحافة الأمامية، أو الحافة الخلفية، أو التكيفية) بناءً على المتطلبات المحددة لتطبيقك. استفد من مكتبات مثل lodash.debounce لتبسيط التنفيذ وتقليل مخاطر الأخطاء. باتباع هذه الإرشادات، يمكنك ضمان أن تطبيقاتك توفر تجربة سلسة وممتعة للمستخدمين في جميع أنحاء العالم.