بناء تطبيقات تفاعلية باستخدام Redux و RxJS و Redux-Observable في React Native

نشرت: 2022-03-11

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

أحد قيود Redux هو أنه لا يدعم السلوك غير المتزامن خارج الصندوق. أحد الحلول لذلك هو redux-observable ، والتي تعتمد على RxJS ، وهي مكتبة قوية للبرمجة التفاعلية في JavaScript. RxJS هو تطبيق لـ ReactiveX ، واجهة برمجة تطبيقات للبرمجة التفاعلية التي نشأت في Microsoft. تجمع ReactiveX بين بعض أقوى ميزات النموذج التفاعلي والبرمجة الوظيفية ونمط المراقب ونمط التكرار.

في هذا البرنامج التعليمي ، سنتعرف على Redux واستخدامه مع React. سنستكشف أيضًا البرمجة التفاعلية باستخدام RxJS وكيف يمكن أن تجعل العمل غير المتزامن الممل والمعقد أمرًا بسيطًا للغاية.

أخيرًا ، سنتعلم مكتبة يمكن ملاحظتها من جديد ، وهي مكتبة تستفيد من RxJS للقيام بعمل غير متزامن ، وبعد ذلك سننشئ تطبيقًا في React Native باستخدام Redux و redux-note.

إعادة

كما يصف نفسه على GitHub ، فإن Redux هي "حاوية حالة يمكن التنبؤ بها لتطبيقات JavaScript." إنه يوفر لتطبيقات JavaScript الخاصة بك حالة عالمية ، مما يبقي الحالة والإجراءات بعيدًا عن مكونات React.

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

يمكن استخدام Redux في React باستخدام رد react-redux ، والذي يوفر ارتباطات لمكونات React لقراءة البيانات من Redux وإجراءات الإرسال لتحديث حالة Redux.

إعادة

يمكن وصف الإعادة بثلاثة مبادئ بسيطة:

1. مصدر واحد للحقيقة

يتم تخزين حالة التطبيق بالكامل في كائن واحد. يتم الاحتفاظ بهذا الكائن في Redux بواسطة متجر. يجب أن يكون هناك متجر واحد في أي تطبيق Redux.

 » console.log(store.getState()) « { user: {...}, todos: {...} }

لقراءة البيانات من Redux في مكون React الخاص بك ، نستخدم وظيفة connect من react-redux . يأخذ connect أربع وسيطات ، وكلها اختيارية. في الوقت الحالي ، سنركز على الأول ، المسمى mapStateToProps .

 /* UserTile.js */ import { connect } from 'react-redux'; class UserTile extends React.Component { render() { return <p>{ this.props.user.name }</p> } } function mapStateToProps(state) { return { user: state.user } } export default connect(mapStateToProps)(UserTile)

في المثال أعلاه ، تستقبل mapStateToProps حالة Redux العامة كأول وسيط لها وتعيد كائنًا سيتم دمجه مع الخاصيات التي تم تمريرها إلى <UserTile /> بواسطة مكونه الأصلي.

2. الحالة للقراءة فقط

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

  1. type أي معرف سلسلة لإجراء. كل عمل يجب أن يكون له عمل فريد.
  2. payload الاختيارية لأي إجراء. يمكن أن يكون في أي وقت ويحتوي على معلومات حول الإجراء.
  3. error يتم تعيين أي خاصية منطقية اختيارية على "صواب" إذا كان الإجراء يمثل خطأ. هذا مشابه لوعد مرفوض Promise. string معرّف Promise. string لإجراء. كل عمل يجب أن يكون له عمل فريد. حسب الاصطلاح ، عندما يكون error true ، يجب أن تكون payload كائن خطأ.
  4. يمكن أن تكون meta Meta أي نوع من القيم. الغرض منه هو أي معلومات إضافية ليست جزءًا من الحمولة.

فيما يلي مثالان على الإجراءات:

 store.dispatch({ type: 'GET_USER', payload: '21', }); store.dispatch({ type: 'GET_USER_SUCCESS', payload: { user: { id: '21', name: 'Foo' } } });

3. تم تغيير الحالة بوظائف خالصة

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

 /* store.js */ import { combineReducers, createStore } from 'redux' function user(state = {}, action) { switch (action.type) { case 'GET_USER_SUCCESS': return action.payload.user default: return state } } function todos(state = [], action) { switch (action.type) { case 'ADD_TODO_SUCCESS': return [ ...state, { id: uuid(), // a random uuid generator function text: action.text, completed: false } ] case 'COMPLETE_TODO_SUCCESS': return state.map(todo => { if (todo.id === action.id) { return { ...todo, completed: true } } return todo }) default: return state } } const rootReducer = combineReducers({ user, todos }) const store = createStore(rootReducer)

على غرار القراءة من الحالة ، يمكننا استخدام وظيفة connect لإرسال الإجراءات.

 /* UserProfile.js */ class Profile extends React.Component { handleSave(user) { this.props.updateUser(user); } } function mapDispatchToProps(dispatch) { return ({ updateUser: (user) => dispatch({ type: 'GET_USER_SUCCESS', user, }), }) } export default connect(mapStateToProps, mapDispatchToProps)(Profile);

RxJS

RxJS

البرمجة التفاعلية

البرمجة التفاعلية هي نموذج برمجة تعريفي يتعامل مع تدفق البيانات في " التدفقات " ومع انتشارها وتغييراتها. RxJS ، مكتبة للبرمجة التفاعلية في JavaScript ، لديها مفهوم الملحوظات ، وهي عبارة عن تدفقات من البيانات يمكن للمراقب الاشتراك فيها ، ويقوم هذا المراقب بتسليم البيانات بمرور الوقت.

مراقب ما يمكن ملاحظته هو كائن له ثلاث وظائف: next ، error ، complete . كل هذه الوظائف اختيارية.

 observable.subscribe({ next: value => console.log(`Value is ${value}`), error: err => console.log(err), complete: () => console.log(`Completed`), })

يمكن أن تحتوي وظيفة .subscribe أيضًا على ثلاث وظائف بدلاً من كائن.

 observable.subscribe( value => console.log(`Value is ${value}`), err => console.log(err), () => console.log(`Completed`) )

يمكننا إنشاء كائن جديد يمكن ملاحظته عن طريق إنشاء كائن observable ، وتمرير وظيفة تستقبل مشتركًا يعرف باسم مراقب. للمشترك ثلاث طرق: next ، error ، complete . يمكن للمشترك الاتصال بعد ذلك بقيمة عدة مرات حسب الحاجة ، complete أو error في النهاية. بعد complete الاتصال أو حدوث error ، لن يدفع المرصود أي قيمة إلى أسفل التدفق.

 import { Observable } from 'rxjs' const observable$ = new Observable(function subscribe(subscriber) { const intervalId = setInterval(() => { subscriber.next('hi'); subscriber.complete() clearInterval(intervalId); }, 1000); }); observable$.subscribe( value => console.log(`Value is ${value}`), err => console.log(err) )

المثال أعلاه سيطبع Value is hi بعد 1000 مللي ثانية.

يمكن أن يصبح إنشاء ما يمكن ملاحظته يدويًا في كل مرة مطولًا ومضجرًا. لذلك ، يحتوي RxJS على العديد من الوظائف لإنشاء ملف. بعض أكثرها شيوعًا هي of ، from ، ajax .

من

يأخذ of سلسلة من القيم ويحولها إلى دفق:

 import { of } from 'rxjs' of(1, 2, 3, 'Hello', 'World').subscribe(value => console.log(value)) // 1 2 3 Hello World

من

from تحويل أي شيء تقريبًا إلى تيار من القيم:

 import { from } from 'rxjs' from([1, 2, 3]).subscribe(console.log) // 1 2 3 from(new Promise.resolve('Hello World')).subscribe(console.log) // 'Hello World' from(fibonacciGenerator).subscribe(console.log) // 1 1 2 3 5 8 13 21 ...

أياكس

يأخذ ajax عنوان URL لسلسلة أو ينشئ عنصرًا يمكن ملاحظته يقوم بإجراء طلب HTTP. يحتوي ajax على وظيفة ajax.getJSON ، والتي تُرجع فقط كائن الاستجابة المتداخل من استدعاء AJAX دون أي خصائص أخرى ajax() :

 import { ajax } from 'rxjs/ajax' ajax('https://jsonplaceholder.typicode.com/todos/1').subscribe(console.log) // {request, response: {userId, id, title, completed}, responseType, status} ajax.getJSON('https://jsonplaceholder.typicode.com/todos/1').subscribe(console.log) // {userId, id, title, completed} ajax({ url, method, headers, body }).subscribe(console.log) // {...}

هناك العديد من الطرق لجعل الملاحظة قابلة للملاحظة (يمكنك رؤية القائمة الكاملة هنا).

العاملين

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

خريطة

تأخذ map دالة وسيطة واحدة وتطبق إسقاطًا على كل عنصر في الدفق:

 import { of } from 'rxjs' import { map } from 'rxjs/operators' of(1, 2, 3, 4, 5).pipe( map(i=> i * 2) ).subscribe(console.log) // 2, 4, 6, 8, 10 

خريطة

منقي

يأخذ filter وسيطة واحدة ويزيل القيم من الدفق التي ترجع خطأ للدالة المحددة:

 import { of } from 'rxjs' import { map, filter } from 'rxjs/operators' of(1, 2, 3, 4, 5).pipe( map(i => i * i), filter(i => i % 2 === 0) ).subscribe(console.log) // 4, 16 

منقي

خريطة مسطحة

يأخذ مشغل flatMap وظيفة تقوم بتعيين كل عنصر في البخار إلى دفق آخر وتسوية جميع قيم هذه التدفقات:

 import { of } from 'rxjs' import { ajax } from 'rxjs/ajax' import { flatMap } from 'rxjs/operators' of(1, 2, 3).pipe( flatMap(page => ajax.toJSON(`https://example.com/blog?size=2&page=${page}`)), ).subscribe(console.log) // [ { blog 1 }, { blog 2 }, { blog 3 }, { blog 4 }, { blog 5 }, { blog 6 } ] 

خريطة مسطحة

دمج

merge يدمج العناصر من دفقين بالترتيب الذي يصلون به:

 import { interval, merge } from 'rxjs' import { pipe, take, mapTo } from 'rxjs/operators' merge( interval(150).pipe(take(5), mapTo('A')), interval(250).pipe(take(5), mapTo('B')) ).subscribe(console.log) // ABAABAABBB 

دمج

قائمة كاملة من المشغلين متوفرة هنا.

يمكن ملاحظته من جديد

يمكن ملاحظته من جديد

حسب التصميم ، تكون جميع الإجراءات في Redux متزامنة. يعد Redux-Observable برنامجًا وسيطًا لـ Redux يستخدم تدفقات يمكن ملاحظتها لأداء عمل غير متزامن ثم إرسال إجراء آخر في Redux مع نتيجة هذا العمل غير المتزامن.

تعتمد إعادة الملحوظة على فكرة الملاحم . الملحمة هي وظيفة تأخذ دفقًا من الإجراءات ، واختيارياً دفق من الحالة وترجع دفقًا من الإجراءات.

الوظيفة (الإجراء $: يمكن ملاحظته ، حالة $: StateObservable ): يمكن ملاحظته ؛

حسب الاصطلاح ، ينتهي كل متغير يمثل تدفقًا (_aka _observable ) بـ $ . قبل أن نتمكن من استخدام إعادة الملحوظة ، علينا إضافتها كبرنامج وسيط في متجرنا. نظرًا لأن الملاحم عبارة عن تيارات من العناصر المرئية ويتم إعادة كل فعل يخرج من هذا البخار إلى التيار ، فإن إعادة نفس الإجراء سيؤدي إلى حلقة لا نهائية.

 const epic = action$ => action$.pipe( filter(action => action.type === 'FOO'), mapTo({ type: 'BAR' }) // not changing the type of action returned // will also result in an infinite loop ) // or import { ofType } from 'redux-observable' const epic = action$ => action$.pipe( ofType('FOO'), mapTo({ type: BAZ' }) )

فكر في هذه البنية التفاعلية كنظام من الأنابيب حيث يتم تغذية خرج كل أنبوب مرة أخرى في كل أنبوب ، بما في ذلك نفسه ، وكذلك في مخفضات Redux. إن المرشحات الموجودة أعلى هذه الأنابيب هي التي تحدد ما يتم إدخاله وما يتم حظره.

دعونا نرى كيف ستعمل ملحمة بينج بونج. يستغرق الأمر ping ، ويرسله إلى الخادم - وبعد اكتمال الطلب - يرسل pong مرة أخرى إلى التطبيق.

 const pingEpic = action$ => action$.pipe( ofType('PING'), flatMap(action => ajax('https://example.com/pinger')), mapTo({ type: 'PONG' }) ) Now, we are going to update our original todo store by adding epics and retrieving users. import { combineReducers, createStore } from 'redux' import { ofType, combineEpics, createEpicMiddleware } from 'redux-observable'; import { map, flatMap } from 'rxjs/operators' import { ajax } from 'rxjs/ajax' // ... /* user and todos reducers defined as above */ const rootReducer = combineReducers({ user, todos }) const epicMiddleware = createEpicMiddleware(); const userEpic = action$ => action$.pipe( ofType('GET_USER'), flatMap(() => ajax.getJSON('https://foo.bar.com/get-user')), map(user => ({ type: 'GET_USER_SUCCESS', payload: user })) ) const addTodoEpic = action$ => action$.pipe( ofType('ADD_TODO'), flatMap(action => ajax({ url: 'https://foo.bar.com/add-todo', method: 'POST', body: { text: action.payload } })), map(data => data.response), map(todo => ({ type: 'ADD_TODO_SUCCESS', payload: todo })) ) const completeTodoEpic = action$ => action$.pipe( ofType('COMPLETE_TODO'), flatMap(action => ajax({ url: 'https://foo.bar.com/complete-todo', method: 'POST', body: { id: action.payload } })), map(data => data.response), map(todo => ({ type: 'COMPLEE_TODO_SUCCESS', payload: todo })) ) const rootEpic = combineEpics(userEpic, addTodoEpic, completeTodoEpic) const store = createStore(rootReducer, applyMiddleware(epicMiddleware)) epicMiddleware.run(rootEpic);

_المهمة: الملاحم مثلها مثل أي تدفقات أخرى يمكن ملاحظتها في RxJS. يمكن أن ينتهي بهم الأمر في حالة كاملة أو خطأ. بعد هذه الحالة ، ستتوقف الملحمة - وتطبيقك - عن العمل. لذلك يجب عليك التقاط كل خطأ محتمل في البخار. يمكنك استخدام عامل التشغيل __catchError__ لهذا الغرض. مزيد من المعلومات: معالجة الخطأ في إعادة الملحوظة .

تطبيق Todo التفاعلي

مع إضافة بعض واجهة المستخدم ، يبدو التطبيق التجريبي (البسيط) شيئًا كالتالي:

تطبيق Todo التفاعلي
الكود المصدري لهذا التطبيق متاح على جيثب. جرب المشروع في Expo أو امسح رمز QR أعلاه في تطبيق Expo.

تلخيص البرنامج التعليمي React و Redux و RxJS

لقد تعلمنا ما هي التطبيقات التفاعلية. تعلمنا أيضًا عن Redux و RxJS وإمكانية إعادة الملحوظة ، كما أنشأنا تطبيق Todo التفاعلي في Expo باستخدام React Native. بالنسبة لمطوري React و React Native ، تقدم الاتجاهات الحالية بعض خيارات إدارة الحالة القوية للغاية.

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