Stale-while-revalidate Data Fetching with React Hooks: دليل
نشرت: 2022-03-11الاستفادة من امتداد HTTP Cache-Control
الذي لا معنى له أثناء إعادة التحقق من الصحة هو أسلوب شائع. يتضمن استخدام الأصول المخزنة مؤقتًا (القديمة) إذا تم العثور عليها في ذاكرة التخزين المؤقت ، ثم إعادة التحقق من ذاكرة التخزين المؤقت وتحديثها بإصدار أحدث من الأصل إذا لزم الأمر. ومن هنا جاء الاسم stale-while-revalidate
.
كيف stale-while-revalidate
عندما يتم إرسال طلب لأول مرة ، يتم تخزينه مؤقتًا بواسطة المتصفح. ثم ، عند إرسال نفس الطلب مرة ثانية ، يتم فحص ذاكرة التخزين المؤقت أولاً. إذا كانت ذاكرة التخزين المؤقت لهذا الطلب متاحة وصالحة ، فسيتم إرجاع ذاكرة التخزين المؤقت كاستجابة. بعد ذلك ، يتم فحص ذاكرة التخزين المؤقت للتأكد من خلوها من التلف ويتم تحديثها إذا وجدت قديمة. يتم تحديد ثبات ذاكرة التخزين المؤقت من خلال قيمة max-age
الموجودة في رأس Cache-Control
جنبًا إلى جنب مع stale-while-revalidate
.
يسمح هذا بتحميل الصفحات بسرعة ، حيث لم تعد الأصول المخزنة مؤقتًا في المسار الحرج. يتم تحميلها على الفور. أيضًا ، نظرًا لأن المطورين يتحكمون في عدد مرات استخدام ذاكرة التخزين المؤقت وتحديثها ، فيمكنهم منع المتصفحات من عرض بيانات قديمة جدًا للمستخدمين.
قد يفكر القراء ، إذا كان بإمكانهم جعل الخادم يستخدم رؤوسًا معينة في ردوده والسماح للمتصفح بأخذها من هناك ، فما الحاجة لاستخدام React و Hooks للتخزين المؤقت؟
اتضح أن نهج الخادم والمتصفح لا يعمل بشكل جيد إلا عندما نريد تخزين محتوى ثابت مؤقتًا. ماذا عن استخدام stale-while-revalidate
لواجهة برمجة تطبيقات ديناميكية؟ من الصعب التوصل إلى قيم جيدة للحد max-age
stale-while-revalidate
في هذه الحالة. غالبًا ما يكون إبطال ذاكرة التخزين المؤقت وجلب استجابة جديدة في كل مرة يتم فيها إرسال طلب هو الخيار الأفضل. هذا يعني بشكل فعال عدم وجود تخزين مؤقت على الإطلاق. لكن مع React و Hooks ، يمكننا أن نفعل ما هو أفضل.
stale-while-revalidate
من واجهة برمجة التطبيقات
لقد لاحظنا أن إعادة التحقق من صحة HTTP stale-while-revalidate
لا تعمل بشكل جيد مع الطلبات الديناميكية مثل استدعاءات واجهة برمجة التطبيقات.
حتى إذا انتهينا من استخدامه ، فسيعيد المتصفح إما ذاكرة التخزين المؤقت أو الاستجابة الجديدة ، وليس كلاهما. هذا لا يسير بشكل جيد مع طلب واجهة برمجة التطبيقات لأننا نرغب في استجابات جديدة في كل مرة يتم فيها إرسال طلب. ومع ذلك ، فإن انتظار ردود جديدة يؤخر سهولة استخدام التطبيق.
إذن ماذا نفعل؟
نقوم بتنفيذ آلية مخصصة للتخزين المؤقت. ضمن ذلك ، اكتشفنا طريقة لإرجاع كل من ذاكرة التخزين المؤقت والاستجابة الجديدة. في واجهة المستخدم ، يتم استبدال الاستجابة المخزنة مؤقتًا باستجابة جديدة عندما تكون متاحة. هكذا سيبدو المنطق:
- عندما يتم إرسال طلب إلى نقطة نهاية خادم API لأول مرة ، قم بتخزين الاستجابة مؤقتًا ثم إعادتها.
- في المرة التالية التي يحدث فيها طلب واجهة برمجة التطبيقات نفسه ، استخدم الاستجابة المخزنة مؤقتًا على الفور.
- بعد ذلك ، أرسل الطلب بشكل غير متزامن لجلب استجابة جديدة. عند وصول الاستجابة ، قم بنشر التغييرات بشكل غير متزامن على واجهة المستخدم وتحديث ذاكرة التخزين المؤقت.
يسمح هذا الأسلوب بتحديثات فورية لواجهة المستخدم - لأن كل طلب واجهة برمجة تطبيقات يتم تخزينه مؤقتًا - ولكنه يسمح أيضًا بالتصحيح النهائي في واجهة المستخدم منذ عرض بيانات الاستجابة الحديثة بمجرد توفرها.
في هذا البرنامج التعليمي ، سنرى نهجًا تدريجيًا حول كيفية تنفيذ ذلك. سنطلق على هذا النهج اسمًا قديمًا أثناء التحديث نظرًا لأن واجهة المستخدم يتم تحديثها بالفعل عندما تحصل على استجابة جديدة.
الاستعدادات: API
لبدء هذا البرنامج التعليمي ، سنحتاج أولاً إلى واجهة برمجة تطبيقات نحضر البيانات منها. لحسن الحظ ، هناك الكثير من خدمات API الوهمية المتاحة. في هذا البرنامج التعليمي ، سنستخدم reqres.in.
البيانات التي نحضرها هي قائمة بالمستخدمين الذين لديهم معلمة استعلام عن page
. هكذا تبدو شفرة الجلب:
fetch("https://reqres.in/api/users?page=2") .then(res => res.json()) .then(json => { console.log(json); });
تشغيل هذا الرمز يعطينا الإخراج التالي. إليك نسخة غير متكررة منه:
{ page: 2, per_page: 6, total: 12, total_pages: 2, data: [ { id: 7, email: "[email protected]", first_name: "Michael", last_name: "Lawson", avatar: "https://s3.amazonaws.com/uifaces/faces/twitter/follettkyle/128.jpg" }, // 5 more items ] }
يمكنك أن ترى أن هذا يشبه واجهة برمجة تطبيقات حقيقية. لدينا ترقيم صفحات في الرد. معلمة استعلام page
مسؤولة عن تغيير الصفحة ، ولدينا إجمالي صفحتين في مجموعة البيانات.
باستخدام API في تطبيق React
دعونا نرى كيف نستخدم API في تطبيق React. بمجرد أن نعرف كيفية القيام بذلك ، سنكتشف جزء التخزين المؤقت. سنستخدم فئة لإنشاء المكون الخاص بنا. ها هو الكود:
import React from "react"; import PropTypes from "prop-types"; export default class Component extends React.Component { state = { users: [] }; componentDidMount() { this.load(); } load() { fetch(`https://reqres.in/api/users?page=${this.props.page}`) .then(res => res.json()) .then(json => { this.setState({ users: json.data }); }); } componentDidUpdate(prevProps) { if (prevProps.page !== this.props.page) { this.load(); } } render() { const users = this.state.users.map(user => ( <p key={user.id}> <img src={user.avatar} alt={user.first_name} style={{ height: 24, width: 24 }} /> {user.first_name} {user.last_name} </p> )); return <div>{users}</div>; } } Component.propTypes = { page: PropTypes.number.isRequired };
لاحظ أننا نحصل على قيمة page
من خلال props
، كما يحدث غالبًا في تطبيقات العالم الحقيقي. أيضًا ، لدينا وظيفة componentDidUpdate
، والتي تعيد تكوين بيانات واجهة برمجة التطبيقات في كل مرة يتغير فيها this.props.page
.
في هذه المرحلة ، تُظهر قائمة من ستة مستخدمين لأن واجهة برمجة التطبيقات تُرجع ستة عناصر لكل صفحة:
إضافة التخزين المؤقت الذي لا معنى له أثناء التحديث
إذا أردنا إضافة التخزين المؤقت الذي لا معنى له أثناء التحديث إلى هذا ، فنحن بحاجة إلى تحديث منطق التطبيق لدينا إلى:
- تخزين استجابة الطلب مؤقتًا بشكل فريد بعد أن يتم جلبه لأول مرة.
- قم بإعادة الاستجابة المخزنة مؤقتًا على الفور إذا تم العثور على ذاكرة التخزين المؤقت للطلب. بعد ذلك ، أرسل الطلب وأعد الاستجابة الجديدة بشكل غير متزامن. أيضًا ، قم بتخزين هذه الاستجابة مؤقتًا في المرة القادمة.
يمكننا القيام بذلك عن طريق امتلاك كائن CACHE
يخزن ذاكرة التخزين المؤقت بشكل فريد. للتميز ، يمكننا استخدام قيمة this.props.page
كمفتاح في كائن CACHE
. بعد ذلك ، نقوم ببساطة بتشفير الخوارزمية المذكورة أعلاه.
import apiFetch from "./apiFetch"; const CACHE = {}; export default class Component extends React.Component { state = { users: [] }; componentDidMount() { this.load(); } load() { if (CACHE[this.props.page] !== undefined) { this.setState({ users: CACHE[this.props.page] }); } apiFetch(`https://reqres.in/api/users?page=${this.props.page}`).then( json => { CACHE[this.props.page] = json.data; this.setState({ users: json.data }); } ); } componentDidUpdate(prevProps) { if (prevProps.page !== this.props.page) { this.load(); } } render() { // same render code as above } }
نظرًا لأنه يتم إرجاع ذاكرة التخزين المؤقت بمجرد العثور عليها وبما أن بيانات الاستجابة الجديدة يتم إرجاعها بواسطة setState
أيضًا ، فهذا يعني أن لدينا تحديثات سلسة لواجهة المستخدم ولا مزيد من وقت الانتظار على التطبيق من الطلب الثاني فصاعدًا. هذا مثالي ، وهو طريقة لا معنى لها أثناء التحديث باختصار.
وظيفة apiFetch
هنا ليست سوى غلاف فوق fetch
حتى نتمكن من رؤية ميزة التخزين المؤقت في الوقت الفعلي. يقوم بذلك عن طريق إضافة مستخدم عشوائي إلى قائمة users
التي يتم إرجاعها بواسطة طلب واجهة برمجة التطبيقات. كما أنه يضيف تأخيرًا عشوائيًا له:
export default async function apiFetch(...args) { await delay(Math.ceil(400 + Math.random() * 300)); const res = await fetch(...args); const json = await res.json(); json.data.push(getFakeUser()); return json; } function delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); }
وظيفة getFakeUser()
هنا مسؤولة عن إنشاء كائن مستخدم مزيف.
مع هذه التغييرات ، أصبحت API الخاصة بنا أكثر واقعية من ذي قبل.
- لها تأخير عشوائي في الاستجابة.
- تقوم بإرجاع بيانات مختلفة قليلاً لنفس الطلبات.
بالنظر إلى هذا ، عندما نغير خاصية page
التي تم تمريرها إلى Component
من المكون الرئيسي لدينا ، يمكننا رؤية التخزين المؤقت لواجهة برمجة التطبيقات قيد التنفيذ. جرب النقر فوق الزر Toggle مرة كل بضع ثوانٍ في CodeSandbox هذا وسترى سلوكًا كالتالي:
إذا نظرت عن كثب ، تحدث بعض الأشياء.
- عندما يبدأ التطبيق ويكون في حالته الافتراضية ، نرى قائمة بسبعة مستخدمين. لاحظ آخر مستخدم في القائمة حيث إنه المستخدم الذي سيتم تعديله عشوائيًا في المرة التالية التي يتم فيها إرسال هذا الطلب.
- عندما نضغط على Toggle لأول مرة ، فإنه ينتظر فترة زمنية صغيرة (400-700 مللي ثانية) ثم يقوم بتحديث القائمة إلى الصفحة التالية.
- الآن ، نحن في الصفحة الثانية. لاحظ مرة أخرى المستخدم الأخير في القائمة.
- الآن ، نضغط على Toggle مرة أخرى ، وسيعود التطبيق إلى الصفحة الأولى. لاحظ أن الإدخال الأخير لا يزال هو نفس المستخدم الذي سجلناه في الخطوة 1 ، ثم يتغير لاحقًا إلى المستخدم الجديد (العشوائي). هذا لأنه ، في البداية ، تم عرض ذاكرة التخزين المؤقت ، ثم بدأت الاستجابة الفعلية.
- نضغط على Toggle مرة أخرى. نفس الظاهرة تحدث. يتم تحميل الاستجابة المخزنة مؤقتًا من المرة الأخيرة على الفور ، ثم يتم جلب البيانات الجديدة ، ولذا نرى آخر تحديث إدخال مما لاحظناه في الخطوة 3.
هذا هو ، التخزين المؤقت الذي لا معنى له أثناء التحديث الذي كنا نبحث عنه. لكن هذا الأسلوب يعاني من مشكلة تكرار التعليمات البرمجية. دعونا نرى كيف ستسير الأمور إذا كان لدينا مكون آخر لجلب البيانات مع التخزين المؤقت. يعرض هذا المكون العناصر بشكل مختلف عن المكون الأول لدينا.
إضافة Stale-while-Refresh إلى مكون آخر
يمكننا القيام بذلك ببساطة عن طريق نسخ المنطق من المكون الأول. يُظهر المكون الثاني قائمة القطط:
const CACHE = {}; export default class Component2 extends React.Component { state = { cats: [] }; componentDidMount() { this.load(); } load() { if (CACHE[this.props.page] !== undefined) { this.setState({ cats: CACHE[this.props.page] }); } apiFetch(`https://reqres.in/api/cats?page=${this.props.page}`).then( json => { CACHE[this.props.page] = json.data; this.setState({ cats: json.data }); } ); } componentDidUpdate(prevProps) { if (prevProps.page !== this.props.page) { this.load(); } } render() { const cats = this.state.cats.map(cat => ( <p key={cat.id} style={{ background: cat.color, padding: "4px", width: 240 }} > {cat.name} (born {cat.year}) </p> )); return <div>{cats}</div>; } }
كما ترى ، فإن منطق المكون المتضمن هنا هو إلى حد كبير نفس المكون الأول. الاختلاف الوحيد هو في نقطة النهاية المطلوبة وأنها تعرض عناصر القائمة بشكل مختلف.
الآن ، نعرض كلا المكونين جنبًا إلى جنب. يمكنك أن ترى أنهم يتصرفون بشكل مشابه:
لتحقيق هذه النتيجة ، كان علينا القيام بالكثير من تكرار الكود. إذا كان لدينا مكونات متعددة مثل هذا ، فسنكرر الكثير من التعليمات البرمجية.
لحلها بطريقة غير مكررة ، يمكن أن يكون لدينا مكون ذو ترتيب أعلى لجلب البيانات وتخزينها مؤقتًا وتمريرها كدعامات. إنها ليست مثالية ولكنها ستنجح. ولكن إذا كان علينا القيام بطلبات متعددة في مكون واحد ، فإن الحصول على مكونات متعددة ذات ترتيب أعلى سيصبح قبيحًا حقًا بسرعة.
بعد ذلك ، لدينا نمط عناصر العرض ، والذي ربما يكون أفضل طريقة للقيام بذلك في مكونات الصنف. إنه يعمل بشكل مثالي ، ولكن مرة أخرى ، فهو عرضة لـ "التفاف الجحيم" ويتطلب منا ربط السياق الحالي في بعض الأحيان. هذه ليست تجربة مطور رائعة ويمكن أن تؤدي إلى الإحباط والأخطاء.

هذا هو المكان الذي تنقذ فيه React Hooks اليوم. إنها تسمح لنا بصياغة منطق المكون في حاوية قابلة لإعادة الاستخدام حتى نتمكن من استخدامه في أماكن متعددة. تم تقديم خطافات React في React 16.8 وهي تعمل فقط مع مكونات الوظيفة. قبل أن نصل إلى التحكم في ذاكرة التخزين المؤقت في React - على وجه الخصوص ، التخزين المؤقت للمحتوى باستخدام الخطافات - دعنا أولاً نرى كيف نقوم بجلب البيانات البسيطة في مكونات الوظيفة.
جلب بيانات API في مكونات الوظيفة
لجلب بيانات API في مكونات الوظيفة ، نستخدم useState
و useEffect
.
يعتبر useState
مشابهًا state
مكونات الفئة و setState
. نستخدم هذا الخطاف للحصول على حاويات ذرية للحالة داخل مكون دالة.
يعد useEffect
لدورة الحياة ، ويمكنك التفكير فيه على أنه مزيج من componentDidMount
و componentDidUpdate
و componentWillUnmount
. يُطلق على المعلمة الثانية التي تم تمريرها إلى useEffect
مصفوفة التبعية. عندما تتغير مصفوفة التبعية ، يتم تشغيل رد النداء الذي تم تمريره كأول وسيط لـ useEffect
مرة أخرى.
إليك كيفية استخدام هذه الخطافات لتنفيذ جلب البيانات:
import React, { useState, useEffect } from "react"; export default function Component({ page }) { const [users, setUsers] = useState([]); useEffect(() => { fetch(`https://reqres.in/api/users?page=${page}`) .then(res => res.json()) .then(json => { setUsers(json.data); }); }, [page]); const usersDOM = users.map(user => ( <p key={user.id}> <img src={user.avatar} alt={user.first_name} style={{ height: 24, width: 24 }} /> {user.first_name} {user.last_name} </p> )); return <div>{usersDOM}</div>; }
من خلال تحديد page
باعتبارها تبعية لاستخدامها ، فإننا نوجه useEffect
لتشغيل رد الاتصال useEffect في كل مرة يتم فيها تغيير page
. هذا هو تماما مثل componentDidUpdate
. أيضًا ، يتم تشغيل useEffect
دائمًا في المرة الأولى ، لذا فهو يعمل مثل componentDidMount
أيضًا.
التي لا معنى لها أثناء التحديث في المكونات الوظيفية
نحن نعلم أن useEffect
يشبه طرق دورة حياة المكون. لذلك يمكننا تعديل وظيفة رد الاتصال التي تم تمريرها إليها لإنشاء التخزين المؤقت الذي لا معنى له أثناء التحديث الذي كان لدينا في مكونات الفصل. يبقى كل شيء كما هو باستثناء خطاف useEffect
.
const CACHE = {}; export default function Component({ page }) { const [users, setUsers] = useState([]); useEffect(() => { if (CACHE[page] !== undefined) { setUsers(CACHE[page]); } apiFetch(`https://reqres.in/api/users?page=${page}`).then(json => { CACHE[page] = json.data; setUsers(json.data); }); }, [page]); // ... create usersDOM from users return <div>{usersDOM}</div>; }
وبالتالي ، لدينا تخزين مؤقت قديم أثناء التحديث يعمل في مكون دالة.
يمكننا أن نفعل الشيء نفسه بالنسبة للمكون الثاني ، أي تحويله إلى وظيفة وتنفيذ التخزين المؤقت الذي لا معنى له أثناء التحديث. ستكون النتيجة مطابقة لما كان لدينا في الفصول الدراسية.
لكن هذا ليس أفضل من مكونات الطبقة ، أليس كذلك؟ لذلك دعونا نرى كيف يمكننا استخدام قوة الخطاف المخصص لإنشاء منطق معياري قديم أثناء التحديث يمكننا استخدامه عبر مكونات متعددة.
خطاف مخصص قديم أثناء التحديث
أولاً ، دعنا نحدد المنطق الذي نريد أن ننتقل إليه في خطاف مخصص. إذا نظرت إلى الكود السابق ، فأنت تعلم أنه الجزء useState
و useEffect
. وبشكل أكثر تحديدًا ، هذا هو المنطق الذي نريد تعديله.
const [users, setUsers] = useState([]); useEffect(() => { if (CACHE[page] !== undefined) { setUsers(CACHE[page]); } apiFetch(`https://reqres.in/api/users?page=${page}`).then(json => { CACHE[page] = json.data; setUsers(json.data); }); }, [page]);
نظرًا لأنه يتعين علينا جعله عامًا ، فسيتعين علينا جعل عنوان URL ديناميكيًا. لذلك نحن بحاجة إلى url
كحجة. سنحتاج أيضًا إلى تحديث منطق التخزين المؤقت ، نظرًا لأن الطلبات المتعددة يمكن أن يكون لها نفس قيمة page
. لحسن الحظ ، عندما يتم تضمين page
في عنوان URL لنقطة النهاية ، فإنها تنتج قيمة فريدة لكل طلب فريد. لذلك يمكننا فقط استخدام عنوان URL بالكامل كمفتاح للتخزين المؤقت:
const [data, setData] = useState([]); useEffect(() => { if (CACHE[url] !== undefined) { setData(CACHE[url]); } apiFetch(url).then(json => { CACHE[url] = json.data; setData(json.data); }); }, [url]);
هذا الى حد كبير ذلك. بعد تغليفها داخل دالة ، سيكون لدينا خطاف مخصص. الق نظرة أدناه.
const CACHE = {}; export default function useStaleRefresh(url, defaultValue = []) { const [data, setData] = useState(defaultValue); useEffect(() => { // cacheID is how a cache is identified against a unique request const cacheID = url; // look in cache and set response if present if (CACHE[cacheID] !== undefined) { setData(CACHE[cacheID]); } // fetch new data apiFetch(url).then(newData => { CACHE[cacheID] = newData.data; setData(newData.data); }); }, [url]); return data; }
لاحظ أننا أضفنا وسيطة أخرى تسمى defaultValue
إليها. يمكن أن تكون القيمة الافتراضية لاستدعاء API مختلفة إذا كنت تستخدم هذا الخطاف في مكونات متعددة. لهذا السبب جعلناها قابلة للتخصيص.
يمكن عمل نفس الشيء لمفتاح data
في كائن newData
. إذا قام الخطاف المخصص بإرجاع مجموعة متنوعة من البيانات ، فقد ترغب في إرجاع newData
وليس newData.data
والتعامل مع هذا الاجتياز من جانب المكون.
الآن بعد أن أصبح لدينا خطافنا المخصص ، والذي يقوم بالرفع الثقيل للتخزين المؤقت القديم أثناء التحديث ، إليك كيفية توصيله بمكوناتنا. لاحظ الكم الهائل من التعليمات البرمجية التي تمكنا من تقليلها. المكون الكامل لدينا الآن هو مجرد ثلاث عبارات. هذا فوز كبير.
import useStaleRefresh from "./useStaleRefresh"; export default function Component({ page }) { const users = useStaleRefresh(`https://reqres.in/api/users?page=${page}`, []); const usersDOM = users.map(user => ( <p key={user.id}> <img src={user.avatar} alt={user.first_name} style={{ height: 24, width: 24 }} /> {user.first_name} {user.last_name} </p> )); return <div>{usersDOM}</div>; }
يمكننا أن نفعل الشيء نفسه للمكون الثاني. سيبدو مثل هذا:
export default function Component2({ page }) { const cats = useStaleRefresh(`https://reqres.in/api/cats?page=${page}`, []); // ... create catsDOM from cats return <div>{catsDOM}</div>; }
من السهل معرفة مقدار الشفرة المعيارية التي يمكننا توفيرها إذا استخدمنا هذا الخطاف. يبدو الرمز أفضل كذلك. إذا كنت ترغب في رؤية التطبيق بالكامل قيد التشغيل ، فانتقل إلى CodeSandbox هذا.
إضافة مؤشر تحميل لاستخدام useStaleRefresh
الآن بعد أن أصبح لدينا الأساسيات ، يمكننا إضافة المزيد من الميزات إلى خطافنا المخصص. على سبيل المثال ، يمكننا إضافة قيمة isLoading
في الخطاف والتي تكون صحيحة كلما تم إرسال طلب فريد وليس لدينا أي ذاكرة تخزين مؤقت لعرضها في هذه الأثناء.
نقوم بذلك من خلال وجود حالة منفصلة لـ isLoading
وفقًا لحالة الخطاف. أي عندما لا يتوفر محتوى ويب مخبأ ، نقوم بتعيينه على true
، وإلا فإننا نضبطه على false
.
هنا هو الخطاف المحدث:
export default function useStaleRefresh(url, defaultValue = []) { const [data, setData] = useState(defaultValue); const [isLoading, setLoading] = useState(true); useEffect(() => { // cacheID is how a cache is identified against a unique request const cacheID = url; // look in cache and set response if present if (CACHE[cacheID] !== undefined) { setData(CACHE[cacheID]); setLoading(false); } else { // else make sure loading set to true setLoading(true); } // fetch new data apiFetch(url).then(newData => { CACHE[cacheID] = newData.data; setData(newData.data); setLoading(false); }); }, [url]); return [data, isLoading]; }
يمكننا الآن استخدام قيمة isLoading
الجديدة في مكوناتنا.
export default function Component({ page }) { const [users, isLoading] = useStaleRefresh( `https://reqres.in/api/users?page=${page}`, [] ); if (isLoading) { return <div>Loading</div>; } // ... create usersDOM from users return <div>{usersDOM}</div>; }
لاحظ أنه عند الانتهاء من ذلك ، ترى نص "تحميل" عند إرسال طلب فريد لأول مرة وعدم وجود ذاكرة تخزين مؤقت.
جعل useStaleRefresh
أي وظيفة غير async
يمكننا أن نجعل الخطاف المخصص لدينا أكثر قوة من خلال جعله يدعم أي وظيفة غير async
بدلاً من مجرد GET
طلبات الشبكة. الفكرة الأساسية وراء ذلك ستبقى كما هي.
- في الخطاف ، تقوم باستدعاء دالة غير متزامنة تقوم بإرجاع قيمة بعد مرور بعض الوقت.
- يتم تخزين كل استدعاء فريد لوظيفة غير متزامنة مؤقتًا بشكل صحيح.
ستعمل سلسلة بسيطة من function.name
arguments
كمفتاح تخزين مؤقت لحالة الاستخدام الخاصة بنا. باستخدام ذلك ، هكذا سيبدو خطافنا:
import { useState, useEffect, useRef } from "react"; import isEqual from "lodash/isEqual"; const CACHE = {}; export default function useStaleRefresh(fn, args, defaultValue = []) { const prevArgs = useRef(null); const [data, setData] = useState(defaultValue); const [isLoading, setLoading] = useState(true); useEffect(() => { // args is an object so deep compare to rule out false changes if (isEqual(args, prevArgs.current)) { return; } // cacheID is how a cache is identified against a unique request const cacheID = hashArgs(fn.name, ...args); // look in cache and set response if present if (CACHE[cacheID] !== undefined) { setData(CACHE[cacheID]); setLoading(false); } else { // else make sure loading set to true setLoading(true); } // fetch new data fn(...args).then(newData => { CACHE[cacheID] = newData; setData(newData); setLoading(false); }); }, [args, fn]); useEffect(() => { prevArgs.current = args; }); return [data, isLoading]; } function hashArgs(...args) { return args.reduce((acc, arg) => stringify(arg) + ":" + acc, ""); } function stringify(val) { return typeof val === "object" ? JSON.stringify(val) : String(val); }
كما ترى ، نحن نستخدم مجموعة من اسم الوظيفة ووسيطاتها المُصنَّفة لتحديد استدعاء دالة بشكل فريد وبالتالي تخزينها مؤقتًا. يعمل هذا مع تطبيقنا البسيط ، لكن هذه الخوارزمية عرضة للتصادمات والمقارنات البطيئة. (مع الحجج غير القابلة للتسلسل ، لن تعمل على الإطلاق.) لذلك بالنسبة لتطبيقات العالم الحقيقي ، فإن خوارزمية التجزئة المناسبة أكثر ملاءمة.
شيء آخر يجب ملاحظته هنا هو استخدام useRef
. يتم استخدام useRef
بالبيانات خلال دورة الحياة الكاملة لمكون التضمين. نظرًا لأن args
عبارة عن مصفوفة - وهي كائن في JavaScript - فإن كل إعادة تصيير للمكون باستخدام الخطاف تتسبب في تغيير مؤشر مرجع args
. لكن args
جزء من قائمة التبعية في أول useEffect
. لذلك يمكن أن يؤدي تغيير args
إلى تشغيل useEffect
حتى عندما لا يتغير شيء. لمواجهة ذلك ، نقوم بإجراء مقارنة عميقة بين Args القديمة والحالية باستخدام args
ونترك فقط رد useEffect
يعمل إذا تغيرت args
بالفعل.
الآن ، يمكننا استخدام خطاف useStaleRefresh
الجديد على النحو التالي. لاحظ التغيير في defaultValue
هنا. نظرًا لأنه خطاف للأغراض العامة ، فإننا لا نعتمد على الخطاف الخاص بنا لإرجاع مفتاح data
في كائن الاستجابة.
export default function Component({ page }) { const [users, isLoading] = useStaleRefresh( apiFetch, [`https://reqres.in/api/users?page=${page}`], { data: [] } ); if (isLoading) { return <div>Loading</div>; } const usersDOM = users.data.map(user => ( <p key={user.id}> <img src={user.avatar} alt={user.first_name} style={{ height: 24, width: 24 }} /> {user.first_name} {user.last_name} </p> )); return <div>{usersDOM}</div>; }
يمكنك العثور على الرمز بالكامل في CodeSandbox هذا.
لا تجعل المستخدمين ينتظرون: استخدم محتوى ذاكرة التخزين المؤقت بشكل فعال مع الخطافات التي لا معنى لها أثناء التحديث والتفاعل
الخطاف useStaleRefresh
الذي أنشأناه في هذه المقالة هو دليل على المفهوم الذي يُظهر ما هو ممكن مع React Hooks. حاول التلاعب بالرمز ومعرفة ما إذا كان يمكنك وضعه في تطبيقك.
بدلاً من ذلك ، يمكنك أيضًا تجربة الاستفادة من التحديث القديم عبر مكتبة مفتوحة المصدر شائعة وجيدة الصيانة مثل swr أو استعلام رد الفعل. كلاهما مكتبات قوية ويدعمان مجموعة من الميزات التي تساعد في طلبات واجهة برمجة التطبيقات.
خطافات React تغير قواعد اللعبة. إنها تسمح لنا بمشاركة منطق المكون بأناقة. لم يكن هذا ممكنًا من قبل لأن حالة المكون وطرق دورة الحياة والعرض تم تجميعها كلها في كيان واحد: مكونات الفئة. الآن ، يمكن أن يكون لدينا وحدات مختلفة لكل منهم. هذا شيء عظيم للتكوين وكتابة كود أفضل. أنا أستخدم مكونات الوظيفة والخطافات لجميع كود React الجديد الذي أكتبه ، وأنا أوصي بشدة بهذا لجميع مطوري React.