استبدال Redux مع ملاحظات ويتفاعل هوكس

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


لماذا نحتاج Redux على الإطلاق؟


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


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



عندما فهمت لأول مرة مفاهيم الإعادة


ما هو الخطأ في الإعادة؟


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


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


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



التشويق ورمز التعلم مكتوبة مع Redux


ملحوظة وربط: نهج بسيط لإدارة الدولة.


استبدال المتجر مع ملاحظتها


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


في TypeScript ، يعد تنفيذ مثل هذه الفئة بسيطًا جدًا:


 type Listener<T> = (val: T) => void; type Unsubscriber = () => void; export class Observable<T> { private _listeners: Listener<T>[]; constructor(private _val: T) {} get(): T { return this._val; } set(val: T) { if (this._val !== val) { this._val = val; this._listeners.forEach(l => l(val)); } } subscribe(listener: Listener<T>): Unsubscriber { this._listeners.push(listener); return () => { this._listeners = this._listeners.filter(l => l !== listener); }; } } 

إذا قارنت هذه الفئة مع Redux Store ، فسترى أنها متشابهة تمامًا: get() يتوافق مع getState() ، subscribe() هو نفسه. الفرق الرئيسي هو أسلوب dispatch() ، والذي تم استبداله بالطريقة الأبسط set() ، والتي تسمح لك بتغيير القيمة الموجودة فيه دون الحاجة إلى الاعتماد على المخفض. الفرق المهم الآخر هو أنه ، على عكس Redux ، سوف نستخدم الكثير من Observable بدلاً من Store واحد يحتوي على جميع الحالات.


استبدال خدمات المخفض


Observable الآن استخدام Observable يمكن Observable لتخزين الحالة العامة ، لكننا ما زلنا بحاجة إلى نقل المنطق الموجود في المخفض. لهذا نستخدم مفهوم الخدمات. الخدمات عبارة عن فصول تنفذ منطق الأعمال بالكامل الخاص بتطبيقاتنا. دعنا نحاول إعادة كتابة المخفض Todo من البرنامج التعليمي Redux إلى خدمة Todo باستخدام Observable :


 import { Observable } from "./observable"; export interface Todo { readonly text: string; readonly completed: boolean; } export enum VisibilityFilter { SHOW_ALL, SHOW_COMPLETED, SHOW_ACTIVE, } export class TodoService { readonly todos = new Observable<Todo[]>([]); readonly visibilityFilter = new Observable(VisibilityFilter.SHOW_ALL); addTodo(text: string) { this.todos.set([...this.todos.get(), { text, completed: false }]); } toggleTodo(index: number) { this.todos.set(this.todos.get().map( (todo, i) => (i === index ? { text: todo.text, completed: !todo.completed } : todo) )); } setVisibilityFilter(filter: VisibilityFilter) { this.visibilityFilter.set(filter); } } 

بمقارنة هذا مع المخفض Todo ، يمكننا ملاحظة الاختلافات التالية:


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

الوصول إلى الخدمات ويمكن ملاحظتها من المكونات


الآن بعد أن استبدلنا "المخزن و المخفض من Redux" بـ "الملاحظة والخدمات" ، نحتاج إلى إتاحة الخدمات من جميع مكونات React. هناك عدة طرق للقيام بذلك: يمكننا استخدام إطار عمل IoC ، على سبيل المثال ، Inversify؛ استخدم سياق React أو استخدم نفس النهج كما في Store Redux - مثيل عمومي واحد لكل خدمة. في هذه المقالة ، سننظر في النهج الأخير:


 import { TodoService } from "./todoService"; export const todoService = new TodoService(); 

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


 import { useEffect, useState } from "react"; import { Observable } from "./observable"; export function useObservable<T>(observable: Observable<T>): T { const [val, setVal] = useState(observable.get()); useEffect(() => { setVal(observable.get()); //   @mayorovp return observable.subscribe(setVal); }, [observable]); return val; } 

وضع كل ذلك معا


مجموعة الأدوات لدينا جاهزة. يمكننا استخدام Observable لتخزين الحالة العامة في الخدمات واستخدام useObservable للتأكد من أن المكونات ستكون دائمًا متزامنة مع هذه الحالة.


دعنا نعيد كتابة مكون TodoList من البرنامج التعليمي Redux باستخدام الخطاف الجديد:


 import React from "react"; import { useObservable } from "./observableHook"; import { todoService } from "./services"; import { Todo, VisibilityFilter } from "./todoService"; export const TodoList = () => { const todos = useObservable(todoService.todos); const filter = useObservable(todoService.visibilityFilter); const visibleTodos = getVisibleTodos(todos, filter); return ( <div> <ul> {visibleTodos.map((todo, index) => ( <TodoItem key={index} todo={todo} index={index} /> ))} </ul> <p> Show: <FilterLink filter={VisibilityFilter.SHOW_ALL}>All</FilterLink>, <FilterLink filter={VisibilityFilter.SHOW_ACTIVE}>Active</FilterLink>, <FilterLink filter={VisibilityFilter.SHOW_ALL}>Completed</FilterLink> </p> </div> ); }; const TodoItem = ({ todo: { text, completed }, index }: { todo: Todo; index: number }) => { return ( <li style={{ textDecoration: completed ? "line-through" : "none", }} onClick={() => todoService.toggleTodo(index)} > {text} </li> ); }; const FilterLink = ({ filter, children }: { filter: VisibilityFilter; children: React.ReactNode }) => { const activeFilter = useObservable(todoService.visibilityFilter); const active = filter === activeFilter; return active ? ( <span>{children}</span> ) : ( <a href="" onClick={() => todoService.setVisibilityFilter(filter)}> {children} </a> ); }; function getVisibleTodos(todos: Todo[], filter: VisibilityFilter): Todo[] { switch (filter) { case VisibilityFilter.SHOW_ALL: return todos; case VisibilityFilter.SHOW_COMPLETED: return todos.filter(t => t.completed); case VisibilityFilter.SHOW_ACTIVE: return todos.filter(t => !t.completed); } } 

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


استنتاج


إذا قارنا هذا الرمز مع منهج Redux ، فسوف نرى العديد من المزايا:


  • الإيجاز: الشيء الوحيد الذي نحتاج إلى القيام به هو التفاف قيم الحالة في useObservable ويمكن استخدام استخدام الخطاف أو useObservable عند الوصول إلى هذه القيم من المكونات. ليست هناك حاجة إلى إعلان إجراء أو منشئ الإجراء أو الكتابة أو الجمع بين المخفض أو توصيل mapStateToProps بمستودع التخزين مع mapDispatchToProps و mapDispatchToProps .
  • البساطة: أصبح من السهل الآن تتبع تنفيذ التعليمات البرمجية. إن فهم ما يحدث بالفعل عند الضغط على زر ما هو مجرد مسألة التبديل إلى تنفيذ الطريقة التي يتم استدعاءها. تم أيضًا تحسين التنفيذ خطوة بخطوة باستخدام مصحح الأخطاء ، حيث لا يوجد مستوى وسيط بين مكوناتنا وخدماتنا.
  • أمان الكتابة خارج الصندوق: لن يحتاج مطورو TypeScript إلى عمل إضافي حتى يكون هناك كود مكتوب بشكل صحيح. ليست هناك حاجة إلى الإعلان عن أنواع الحالة لكل إجراء.
  • دعم المزامنة / الانتظار: على الرغم من عدم توضيح ذلك هنا ، يعمل هذا الحل بشكل رائع مع الوظائف غير المتزامنة ، مما يعمل على تبسيط البرمجة غير المتزامنة. لا حاجة للاعتماد على البرامج الوسيطة ، مثل redux-thunk ، والتي تتطلب معرفة عميقة بالبرمجة الوظيفية لفهمها.

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


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


الملاحظات


فئة Observable في هذا المنشور بسيطة جداً. يمكن استبداله بتطبيقات أكثر تقدماً مثل الملاحظات الصغيرة (مكتبتنا الخاصة) أو RxJS.


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


من مترجم: هذا المنشور ، الذي يمكن اعتباره مقدمة عن إدارة الحالة باستخدام Observable ، هو امتداد للموضوع الذي تمت تغطيته في مقالة إدارة حالة التطبيق مع RxJS / Immer كبديل بسيط لـ Redux / MobX ، الذي يصف كيفية تبسيط استخدام هذا النهج.

Source: https://habr.com/ru/post/ar484952/


All Articles