إدارة حالة التطبيق باستخدام RxJS / Immer كبديل بسيط لـ Redux / MobX

"سوف تفهم متى تحتاج إلى Flux. إذا لم تكن متأكدًا من أنك في حاجة إليه ، فلن تحتاج إليه." بيت هانت


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


state = {value: 0} increase() { state.value += 1 } decrease() { state.value -= 1 } 

يبدو على الفور أن MobX يمكنها تقديم مثل هذا الحل ، فلماذا لا تستخدمه؟ بعد العمل مع MobX لفترة ، توصلت إلى استنتاج مفاده أنه من الأسهل بالنسبة لي شخصياً أن أعمل مع سلسلة من الحالات غير القابلة للتغيير (مثل Redux) من منطق الحالة القابلة للتغيير (مثل MobX) ، ولن أسمي مطبخها الداخلي بالبساطة.


بشكل عام ، أردت أن أجد حلاً بسيطًا لإدارة الحالة ، والذي يستند إلى المناعة ، مع إمكانية استخدامه في Angular \ React وتطبيقه على TypeScript. لم تقدم المراجعة السريعة على المساحات المفتوحة github حلاً مناسبًا ، لذلك دعونا نأخذ RxJS / Immer ونحاول القيام به.


نحن نستخدم RxJS


سنأخذ BehaviorSubjet كأساس ، والذي getValue تدفق التغييرات في الحالة {value: 0} -> {value: 1} -> {value: 2} والذي يحتوي أيضًا على طريقة getValue التي يمكنك من خلالها الحصول على الحالة الحالية. إذا قارنت API BehaviorSubject بـ Redux


  • getValue() / getState() //
  • subscribe() / subscribe() //
  • next(value) / dispatch(action), replaceReducer(nextReducer) //

قد تلاحظ أنها متشابهة للغاية. الفرق الرئيسي هو أنه في BehaviorSubject بدلاً من Action/Reducer يمكن تعيين الحالة الجديدة باستخدام الطريقة next() .


بالنسبة إلى المثال المضاد أعلاه ، قد يبدو التنفيذ كما يلي:


CounterService V1


 class CounterServiceV1 { state = new BehaviorSubject({value: 0}) increase() { this.state.next({value: this.state.value.value + 1}) } decrease() { this.state.next({value: this.state.value.value - 1}) } } 

إن التكرار في التكرار من this.state.next التغييرات التي تطرأ على الحالة أمر مذهل. هذا يختلف تمامًا عن حالة النتيجة المرغوبة. state.value += 1


أضف إمير


لتبسيط التغيير في حالة المناعة ، سوف نستخدم مكتبة Immer. يتيح لك Immer إنشاء حالة جديدة غير قابلة للتغيير بسبب طفرة التيار. إنه يعمل بهذه الطريقة:


 const state = {value: 0} //     const draft = createDraft(state) //      draft.value += 1 //    const newState = finishDraft(draft) 

حزمة من السلوك Subject و Immer


قم بلف استخدام BehaviorSubject و Immer في صفنا الخاص RxState :


 class RxState<TState> { private subject$: BehaviorSubject<TState> private currentDraft?: Draft<TState> get state() { return this.subject$.value } get state$() { return this.subject$ } get draft(): Draft<TState> { if (this.currentDraft !== undefined) { return this.currentDraft } throw new Error("draft doesn't exists") } constructor(readonly initialState: TState) { this.subject$ = new BehaviorSubject(initialState) } public updateState(recipe: (draft: Draft<TState>) => void) { let topLevelUpdate = false //     updateState if (!this.currentDraft) { this.currentDraft = createDraft(this.state) topLevelUpdate = true } recipe(this.currentDraft) if (!topLevelUpdate) { return } const newState = finishDraft(this.currentDraft, () => {}) as TState this.currentDraft = undefined if (newState !== this.state) { this.subject$.next(newState) } } } 

باستخدام RxState ، نعيد كتابة CounterService :


CounterService V2


 class CounterServiceV2 { state = new RxState({value: 0}) increase() { this.state.updateState(draft => { draft.value += 1 }) } decrease() { this.state.updateState(draft => { draft.value -= 1 }) } } 

فرق
 - state = new BehaviorSubject({value: 0}) + state = new RxState({value: 0}) increase() { - this.state.next({value: this.state.value.value + 1}) + this.state.updateState(draft => { + draft.value += 1 + }) } decrease() { - this.state.next({value: this.state.value.value - 1}) + this.state.updateState(draft => { + draft.value -= 1 + }) } 

يبدو أفضل قليلاً من الخيار الأول ، ولكن لا تزال هناك حاجة للاتصال بـ updateState كل مرة. لحل هذه المشكلة ، قم بإنشاء فصل آخر وقم SimpleImmutableStore ، وسيكون الأساس للقصة.


 class SimpleImmutableStore<TState> { rxState!: RxState<TState> get draft() { return this.rxState.draft } constructor(initialState: TState) { this.rxState = new RxState<TState>(initialState) } public updateState(recipe: (draft: Draft<TState>) => void) { this.rxState.updateState(recipe) } } 

ننفذ المتجر بمساعدته:


CounterStore V1


 class CounterStoreV1 extends SimpleImmutableStore<{value: number}> { constructor(){ super({value: 0}) } increase() { this.updateState(() => { this.draft.value += 1 }) } decrease() { this.updateState(() => { this.draft.value -= 1 }) } } 

فرق
 -class CounterServiceV2 { - state = new RxState({value: 0}) +class CounterStoreV1 extends SimpleImmutableStore<{value: number}> { + constructor(){ + super({value: 0}) + } increase() { - this.state.updateState(draft => { - draft.value += 1 + this.updateState(() => { + this.draft.value += 1 }) } decrease() { - this.state.updateState(draft => { - draft.value -= 1 + this.updateState(() => { + this.draft.value -= 1 }) } } 

كما ترون ، لم يتغير أي شيء بشكل كبير ، ولكن الآن جميع الطرق لديها رمز شائع في شكل غلاف this.updateState . للتخلص من هذا الازدواجية ، نكتب دالة تلتف جميع أساليب الفصل في مكالمة updateState :


 const wrapped = Symbol() //     function getMethodsNames(constructor: any) { const names = Object.getOwnPropertyNames(constructor.prototype).filter( x => x !== "constructor" && typeof constructor.prototype[x] === "function", ) return names } function wrapMethodsWithUpdateState(constructor: any) { if (constructor[wrapped]) { return } constructor[wrapped] = true for (const propertyName of getMethodsNames(constructor)) { const descriptor = Object.getOwnPropertyDescriptor( constructor.prototype, propertyName, )! const method = descriptor.value descriptor.value = function(...args: any[]) { const store = this as SimpleImmutableStore<any> let result: any store.updateState(() => { //     updateState result = method.call(store, ...args) }) return result } Object.defineProperty(constructor.prototype, propertyName, descriptor) } } 

وسوف نسميها في المنشئ (إذا رغبت في ذلك ، يمكن أيضًا تنفيذ هذه الطريقة كديكور للصف)


  constructor(initialState: TState ) { this.rxState = new RxState<TState>(initialState) wrapMethodsWithUpdateState(this.constructor) } 

CounterStore


النسخة النهائية للقصة. setValue ، أضف منطقًا صغيرًا setValue طرق أخرى لتمرير معلمة setValue التزامن:


 class CounterStore extends SimpleImmutableStore<{ value: number }> { constructor() { super({value: 0}) } increase() { this.draft.value += 1 } decrease() { const newValue = this.draft.value - 1 if (newValue >= 0) { this.draft.value = newValue } } setValue(value: number) { this.draft.value = value } increaseWithDelay() { setTimeout(() => this.increase(), 300) } } 

استخدام مع الزاوي


نظرًا لأن RxJS هو أساس الرصة الناتجة ، يمكن استخدامه مع الزاوي بالتزامن مع ماسورة غير متزامنة:


 <div *ngIf="store.rxState.state$ | async as state"> <span>{{state.value}}</span> <button (click)="store.increase()">+</button> <button (click)="store.decrease()">-</button> <button (click)="store.setValue(0)">Reset</button> <button (click)="store.increaseWithDelay()">Increase with delay</button> </div> 

عرض


استخدم مع React


ل React ، سنكتب هوك مخصص:


 function useStore<TState, TResult>( store: SimpleImmutableStore<TState>, project: (store: TState) => TResult, ): TResult { const projectRef = useRef(project) useEffect(() => { projectRef.current = project }, [project]) const [state, setState] = useState(projectRef.current(store.rxState.state)) useEffect(() => { const subscription = store.rxState.state$.subscribe(value => { const newState = projectRef.current(value) if (!shallowEqual(state, newState)) { setState(newState) } }) return () => { subscription.unsubscribe() } }, [store, state]) return state } 

عنصر


 const Counter = () => { const store = useMemo(() => new CounterStore(), []) const value = useStore(store, x => x.value) return ( <div className="counter"> <span>{value}</span> <button onClick={() => store.increase()}>+</button> <button onClick={() => store.decrease()}>-</button> <button onClick={() => store.setValue(0)}>Reset</button> <button onClick={() => store.increaseWithDelay()}>Increase with delay</button> </div> ) } 

عرض


استنتاج


والنتيجة هي حل بسيط وعملي إلى حد ما ، والذي أستخدمه بشكل دوري في مشاريعي. إذا كنت ترغب في ذلك ، يمكنك إضافة العديد من الفوائد إلى هذا الجانب: البرامج الوسيطة ، وتقطيع الحالة ، وتحديث الاستعادة - ولكن هذا يتجاوز نطاق هذه المقالة بالفعل. يمكن العثور على نتيجة هذه الإضافات على github https://github.com/simmor-store/simmor


سأكون ممتنا لأية اقتراحات وتعليقات.

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


All Articles