بعد إتقان الخطافات ، عانى العديد من مطوري React من النشوة ، وحصلوا أخيرًا على مجموعة أدوات بسيطة ومريحة تتيح لك تنفيذ المهام باستخدام رمز أقل بكثير. ولكن هل هذا يعني أن استخدام السنانير useState andReducer القياسية التي يتم توفيرها خارج الصندوق هي كل ما نحتاجه لإدارة الحالة؟
في رأيي ، في شكلها الخام ، فإن استخدامها ليس ملائمًا للغاية ، ويمكن على الأرجح اعتبارها أساسًا لبناء روابط إدارة الدولة المريحة حقًا. المطورين React أنفسهم تشجيع بقوة تطوير السنانير المخصصة ، فلماذا لا تفعل ذلك؟ تحت الخفض ، سننظر في مثال بسيط للغاية ومفهوم لما هو خطأ في السنانير العادية وكيف يمكن تحسينها ، لدرجة أنها ترفض تمامًا استخدامها في شكلها النقي.
هناك مجال معين للإدخال ، اسم مشروط. وهناك زر بالنقر فوق الذي يجب علينا تقديم طلب إلى الخادم مع الاسم الذي تم إدخاله (بحث معين). يبدو أنه يمكن أن يكون أسهل؟ ومع ذلك ، فإن الحل أبعد ما يكون عن الوضوح. أول تطبيق ساذج:
const App = () => { const [name, setName] = useState(''); const [request, setRequest] = useState(); const [result, setResult] = useState(); useEffect(() => { fetch('//example.api/' + name).then((data) => { setResult(data.result); }); }, [request]); return <div> <input onChange={e => setName(e.target.value)}/> <input type="submit" value="Check" onClick={() => setRequest(name)}/> { result && <div>Result: { result }</div> } </div>; }
ما هو الخطأ هنا؟ إذا قام المستخدم ، بإدخال شيء ما في الحقل ، بإرسال النموذج مرتين ، فلن يعمل إلا الطلب الأول بالنسبة لنا ، لأن عند النقر فوق الثاني لن يتغير ولن يعمل useEffect. إذا تخيلنا أن طلبنا هو خدمة البحث عن التذاكر ، وقد يقوم المستخدم بإرسال الاستمارة في بعض الفواصل مرارًا وتكرارًا دون إجراء تغييرات ، فلن ينجح هذا التطبيق بالنسبة لنا! يعد استخدام الاسم كتبعية للاستخدام useEffect أيضًا أمرًا غير مقبول ، وإلا سيتم إرسال النموذج فورًا عندما يتغير النص. حسنا ، عليك أن تظهر براعة.
const App = () => { const [name, setName] = useState(''); const [request, setRequest] = useState(); const [result, setResult] = useState(); useEffect(() => { fetch('//example.api/' + name).then((data) => { setResult(data.result); }); }, [request]); return <div> <input onChange={e => setName(e.target.value)}/> <input type="submit" value="Check" onClick={() => setRequest(!request)}/> { result && <div>Result: { result }</div> } </div>; }
الآن ، مع كل نقرة ، سنقوم بتغيير معنى الطلب إلى الاتجاه المعاكس ، الأمر الذي سيحقق السلوك المطلوب. هذا عكاز صغير جدًا وبريء ، ولكنه يجعل الكود مربكًا إلى حد ما لفهمه. ربما يبدو لك الآن أنني أمتص المشكلة من إصبعي وأضخّم حجمها. حسنًا ، للإجابة عما إذا كان هذا صحيحًا أم لا ، يلزمك مقارنة هذا الرمز بالتطبيقات الأخرى التي تقدم طريقة تعبيرية أكثر.
لنلقِ نظرة على هذا المثال على المستوى النظري باستخدام تجريد الخيط. أنها مريحة للغاية لوصف حالة واجهات المستخدم. لذلك ، لدينا دفقان: البيانات التي يتم إدخالها في حقل النص (الاسم $) ، ومجموعة من النقرات على زر إرسال النموذج (انقر فوق $). نحتاج منهم إلى إنشاء دفق ثالث مدمج للطلبات إلى الخادم.
name$ __(C)____(Ca)_____(Car)____________________(Carl)___________ click$ ___________________________()______()________________()_____ request$ ___________________________(Car)___(Car)_____________(Carl)_
هنا هو السلوك الذي نحتاج إلى تحقيقه. كل تيار له جانبان: القيمة التي يتمتع بها ، والنقطة الزمنية التي تتدفق خلالها القيم. في المواقف المختلفة ، قد نحتاج إلى جانب أو آخر أو كلاهما. يمكنك مقارنة هذا بالإيقاع والانسجام في الموسيقى. وتسمى التدفقات التي يكون زمن الاستجابة فيها ضروريًا أيضًا الإشارات.
في حالتنا ، انقر $ هو إشارة صافية: لا يهم القيمة التي تتدفق عبرها (غير محدد / صحيح / حدث / أيا كان) ، فهي مهمة فقط عندما يحدث هذا. اسم القضية $
العكس: تغييراته لا تنطوي بأي حال من الأحوال على أي تغييرات في النظام ، لكننا قد نحتاج إلى معناها في مرحلة ما. ومن هذين المسارين ، نحتاج إلى جعل الثالث ، مع الأخذ من أول مرة ، من الثانية - القيمة.
في حالة Rxjs ، لدينا مشغل جاهز تقريبًا لهذا:
const names$ = fromEvent(...); const click$ = fromEvent(...); const request$ = click$.pipe(withLatestFrom(name$), map(([name]) => fromPromise(fetch(...))));
ومع ذلك ، يمكن أن يكون الاستخدام العملي لـ Rx في React غير مريح تمامًا. الخيار الأكثر ملاءمة هو مكتبة mrr ، المبنية على نفس المبادئ الوظيفية التفاعلية مثل Rx ، ولكن تم تكييفها خصيصًا للاستخدام مع React على مبدأ "التفاعل الكلي" ومتصلة كخطاف.
import useMrr from 'mrr/hooks'; const App = props => { const [state, set] = useMrr(props, { result: [name => fetch('//example.api/' + name).then(data => data.result), '-name', 'submit'], }); return <div> <input value={state.name} onChange={set('name')}/> <input type="submit" value="Check" onClick={set('submit')}/> { state.result && <div>Result: { state.result }</div> } </div>; }
تشبه واجهة useMrr useState أو useReducer: تقوم بإرجاع كائن حالة (قيم كافة مؤشرات الترابط) و setter من أجل وضع القيم في مؤشرات الترابط. لكن في الداخل ، كل شيء مختلف قليلاً: كل حقل حالة (= دفق) ، باستثناء الحقول التي نضع فيها القيم مباشرة من أحداث DOM ، يتم وصفه بواسطة دالة وقائمة من سلاسل العمليات الرئيسية ، والتي سيؤدي تغييرها إلى إعادة حساب الطفل. في هذه الحالة ، سيتم استبدال قيم سلاسل العمليات الرئيسية بالوظيفة. إذا كنا نريد فقط الحصول على قيمة الدفق ، ولكن لا نستجيب لتغييره ، نكتب "ناقص" أمام الاسم ، كما في حالة الاسم.
حصلنا على السلوك المرغوب فيه ، في جوهره ، في سطر واحد. ولكن هذا ليس مجرد الإيجاز. دعونا نقارن النتائج التي تم الحصول عليها بمزيد من التفاصيل ، وقبل كل شيء فيما يتعلق بمعلمة مثل سهولة القراءة والوضوح للشفرة الناتجة.
في السيد ، ستتمكن من فصل "المنطق" تمامًا تقريبًا عن "القالب": لن تضطر إلى كتابة أي معالجات ضرورية معقدة في JSX. كل شيء معلن للغاية: إننا ببساطة نقوم بتعيين حدث DOM إلى الدفق المقابل ، عملياً دون تحويل (بالنسبة لحقول الإدخال ، يتم استخراج القيمة e.target.value تلقائيًا ، ما لم تحدد خلاف ذلك) ، وفي بنية useMrr بالفعل ، نصف كيفية تشكيل التدفقات الأساسية شركة تابعة. وبالتالي ، في حالة كل من تحويلات البيانات المتزامنة وغير المتزامنة ، يمكننا دائمًا تتبع كيفية تشكيل القيمة لدينا بسهولة.
مقارنة مع Px: لم يكن لدينا حتى استخدام عوامل تشغيل إضافية: إذا نتيجة لذلك ، تلقت وظائف mrr وعدًا ، فسوف تنتظر تلقائيًا حتى يتم حلها ووضع البيانات المستلمة في الدفق. أيضا ، بدلا من withLestFrom ، استخدمنا
الاستماع السلبي (علامة الطرح) ، والذي هو أكثر ملاءمة. تخيل أنه بالإضافة إلى الاسم ، سنحتاج إلى إرسال حقول أخرى. ثم في السيد نضيف تيار الاستماع السلبي آخر:
result: [(name, surname) => fetch(...), '-name', '-surname', 'submit'],
وفي Rx ، عليك نحت واحدة أخرى باستخدام أحدث من خريطة ، أو أولاً الجمع بين الاسم واللقب في دفق واحد.
لكن العودة إلى السنانير والسيد. ربما يكون السجل الأكثر قابلية للقراءة للتبعيات ، والذي يوضح دائمًا كيف يتم تكوين البيانات ، أحد المزايا الرئيسية. لا تسمح واجهة useEffect الحالية بشكل أساسي بالرد على تدفقات الإشارات ، وهذا هو السبب
لا بد لي من الخروج مع التقلبات المختلفة.
نقطة أخرى هي أن خيار السنانير العادية يحمل الاداءات اضافية. إذا نقر المستخدم للتو على الزر ، فإن هذا لا يستلزم بعد أي تغييرات على واجهة المستخدم والتي يحتاج رد الفعل إلى رسمها. ومع ذلك ، سيتم استدعاء تقديم. في المتغير مع mrr ، لن يتم تحديث الحالة التي يتم إرجاعها إلا عند وصول استجابة من الخادم بالفعل. حفظ في المباريات ، تقول؟ حسنا ، ربما. لكن بالنسبة لي شخصياً ، فإن مبدأ "إعادة تقديم نفسك في أي موقف غير مفهوم" ، والذي هو أساس الخطافات الأساسية ، يؤدي إلى الرفض.
التجسيد الإضافي يعني تشكيلًا جديدًا لمعالجات الأحداث. بالمناسبة ، هنا السنانير العادية كلها سيئة. لا يقتصر الأمر على المعالجين فحسب ، بل يجب أيضًا تجديدهم في كل مرة يقدمون فيها. ولن يكون من الممكن استخدام التخزين المؤقت بشكل كامل هنا ، لأن يجب قفل العديد من معالجات المتغيرات الداخلية للمكون. تعد معالجات mrr أكثر إقرارًا ، كما أن التخزين المؤقت مدمج بالفعل مع mrr: سيتم تعيين ('name') مرة واحدة فقط ، وسيتم استبداله من ذاكرة التخزين المؤقت لعمليات التجسيد اللاحقة.
مع زيادة قاعدة الشفرة ، يمكن أن تصبح المعالجات الضرورية أكثر تعقيدًا. دعنا نقول أننا بحاجة أيضًا إلى إظهار عدد عمليات إرسال النماذج التي قدمها المستخدم.
const App = () => { const [request, makeRequest] = useState(); const [name, setName] = useState(''); const [result, setResult] = useState(false); const [clicks, setClicks] = useState(0); useEffect(() => { fetch('//example.api/' + name).then((data) => { setResult(data.result); }); }, [request]); return <div> <input onChange={e => setName(e.target.value)}/> <input type="submit" value="Check" onClick={() => { makeRequest(!request); setClicks(clicks + 1); }}/><br /> Clicked: { clicks } </div>; }
ليس لطيفا جدا يمكنك بالطبع تقديم المعالج كدالة منفصلة داخل المكون. ستزداد قابلية القراءة ، ولكن تبقى مشكلة تجديد الوظيفة مع كل تجسيد ، بالإضافة إلى مشكلة النقص. في جوهرها ، يعد هذا رمزًا إجرائيًا منتظمًا ، على الرغم من الاعتقاد السائد بأن واجهة برمجة تطبيقات React تتغير تدريجياً نحو نهج وظيفي.
بالنسبة لأولئك الذين يبدو أن حجم المشكلة مبالغ فيه ، يمكنني الإجابة على ذلك ، على سبيل المثال ، مطورو React أنفسهم على دراية بمشكلة الجيل المفرط من المتعهدين ، حيث يقدمون لنا على الفور عكازًا في شكل useCallback.
على السيد:
const App = props => { const [state, set] = useMrr(props, { $init: { clicks: 0, }, isValid: [name => fetch('//example.api/' + name).then(data => data.isValid), '-name', 'makeRequest'], clicks: [a => a + 1, '-clicks', 'makeRequest'], }); return <div> <input onChange={set('name')}/> <input type="submit" value="Check" onClick={set('makeRequest')}/> </div>; }
البديل الأكثر ملاءمة هو useReducer ، مما يسمح لك بالتخلي عن حتمية المعالجات. ولكن لا تزال هناك مشاكل مهمة أخرى: قلة العمل مع الإشارات (حيث أن الاستخدام نفسه سيكون مسؤولاً عن الآثار الجانبية) ، وكذلك أسوأ قابلية للقراءة أثناء التحويلات غير المتزامنة (بمعنى آخر ، يصعب تتبع العلاقة بين حقول المتجر ، بسبب الاستخدام نفسهتأثير ). إذا كان الرسم البياني للاعتماد بين حقول الحالة (مؤشرات الترابط) مرئيًا بشكل واضح في السيد ، في السنانير ، عليك إدارة عينيك لأعلى ولأسفل قليلاً.
كما أن مشاركة useState و useReducer في نفس المكون ليست مريحة للغاية (مرة أخرى سيكون هناك معالجات حتمية معقدة من شأنها تغيير شيء في useState
والإرسال إيفاد) ، بسبب ، على الأرجح ، قبل تطوير المكون ، سوف تحتاج إلى قبول خيار واحد أو آخر.
بالطبع ، لا يزال يمكن النظر في جميع الجوانب. حتى لا أتجاوز نطاق المقالة ، سأتطرق إلى بعض النقاط الأقل أهمية بالكامل.
تسجيل مركزي ، تصحيح. نظرًا لأن جميع التدفقات في mrr موجودة في لوحة وصل واحدة ، فإن تصحيح الأخطاء يكفي لإضافة علامة واحدة:
const App = props => { const [state, set] = useMrr(props, { $log: true, $init: { clicks: 0, }, isValid: [name => fetch('//example.api/' + name).then(data => data.isValid), '-name', 'makeRequest'], clicks: [a => a + 1, '-clicks', 'makeRequest'], }); ...
بعد ذلك ، سيتم عرض جميع التغييرات على المواضيع في وحدة التحكم. للوصول إلى الحالة بأكملها (أي القيم الحالية لجميع سلاسل الرسائل) ، هناك حالة $ زائفة:
a: [({ name, click, result }) => { ... }, '$state', 'click'],
وبالتالي ، إذا كنت بحاجة أو إذا كنت معتادًا على أسلوب التحرير ، فيمكنك الكتابة بأسلوب المحرر في mrr ، وإرجاع قيمة حقل جديدة استنادًا إلى الحدث والحالة السابقة بأكملها. لكن العكس (الكتابة على useReducer أو محرر في أسلوب السيد) لن ينجح ، بسبب عدم وجود تفاعل في هذه.
العمل مع الوقت. تذكر جانبين من التدفقات: المعنى ووقت الاستجابة والانسجام والإيقاع؟ لذا ، فإن العمل مع الأول في السنانير العادية بسيط ومريح للغاية ، ولكن مع الثاني - لا. من خلال العمل بمرور الوقت ، أعني تكوين تيارات الأطفال ، والتي يختلف "إيقاعها" عن الوالد. هذا هو في المقام الأول جميع أنواع المرشحات ، و debowns ، trotl ، إلخ. كل هذا على الأرجح سوف تضطر إلى تنفيذ نفسك. في السيد ، يمكنك استخدام بيانات جاهزة من خارج منطقة الجزاء. السيد مجموعة السيد هو أدنى من مجموعة متنوعة من مشغلي Rx ، ولكن لديها تسمية أكثر سهولة.
التفاعل المشترك. أتذكر أنه في المحرر ، كان من الممارسات الجيدة إنشاء قصة واحدة فقط. إذا كنا نستخدم استخدام المخفض في العديد من المكونات ،
قد تكون هناك مشكلة في تنظيم التفاعل بين الطرفين. على السيد ، يمكن للتدفقات "التدفق" بحرية من عنصر إلى آخر إما أعلى أو أسفل التسلسل الهرمي ، ولكن هذا لن يخلق مشاكل بسبب النهج التعريفي. مزيد من التفاصيل
يتم وصف هذا الموضوع ، بالإضافة إلى ميزات واجهة برمجة تطبيقات mrr ، في المقالة Actors + FRP in React
النتائج
خطافات رد الفعل الجديدة رائعة وتبسط حياتنا ، لكن لديها بعض العيوب التي يمكن إصلاح خطاف عام للأغراض العامة (إدارة الحالة). اقترح UseMrr من مكتبة mrr الوظيفية التفاعلية ويعتبر على هذا النحو.
المشاكل وحلولها:
- عمليات إعادة حساب غير ضرورية للبيانات في كل تقديم (في mrr غائب بسبب التفاعل القائم على الدفع)
- الاداءات الاضافية عندما لا يستلزم تغيير في الحالة تغيير في واجهة المستخدم
- ضعف قراءة التعليمات البرمجية مع التحويلات غير المتزامنة (مقارنة بالتحويلات المتزامنة). في السيد ، الكود غير المتزامن ليس أدنى من المتزامن في قابلية القراءة والتعبير. معظم القضايا التي نوقشت في مقال أخير حول useEffect on mrr مستحيل بشكل أساسي
- معالجات حتمية غير قابلة للتخزين المؤقت دائمًا (في السيد ، يتم تخزينها مؤقتًا تلقائيًا ، يمكن دائمًا تخزينها مؤقتًا وتعليميًا)
- باستخدام useState و useReducer في نفس الوقت يمكن إنشاء رمز حرج
- عدم وجود أدوات لتحويل التدفقات بمرور الوقت (الانحدار ، الخانق ، حالة السباق)
في العديد من النقاط ، يمكن للمرء أن يجادل بأنه يمكن حلها عن طريق السنانير المخصصة. ولكن هذا هو بالضبط ما يتم اقتراحه ، ولكن بدلاً من التطبيقات المتباينة ، يُقترح حل كلي ومتسق لكل مهمة منفصلة.
أصبحت العديد من المشكلات مألوفة جدًا بالنسبة لنا حتى لا يتم الاعتراف بها بوضوح. على سبيل المثال ، تبدو التحويلات غير المتزامنة دائمًا أكثر تعقيدًا وإرباكًا من التحويلات المتزامنة ، والروابط في هذا المعنى ليست أسوأ من الأساليب السابقة (eds ، وما إلى ذلك). لإدراك ذلك كمشكلة ، يجب أولاً أن ترى الأساليب الأخرى التي تقدم حلاً أفضل.
تهدف هذه المقالة إلى عدم فرض أي وجهات نظر محددة ، بل لفت الانتباه إلى المشكلة. أنا متأكد من وجود حلول أخرى أو يتم إنشاؤها والتي يمكن أن تصبح بديلاً جيدًا ، لكن لم تصبح معروفة على نطاق واسع. واجهة برمجة تطبيقات React Cache القادمة يمكنها أيضًا أن تحدث فرقًا كبيرًا. سأكون سعيدًا بالنقد والمناقشة في التعليقات.
يمكن للمهتمين مشاهدة عرض تقديمي حول هذا الموضوع على kyivjs في 28 مارس.