"سوف تفهم متى تحتاج إلى 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}
حزمة من السلوك 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
باستخدام 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()
وسوف نسميها في المنشئ (إذا رغبت في ذلك ، يمكن أيضًا تنفيذ هذه الطريقة كديكور للصف)
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
سأكون ممتنا لأية اقتراحات وتعليقات.