使用RxJS / Immer管理应用程序状态,作为Redux / MobX的简单替代方案

“您会在需要Flux时了解。如果不确定是否需要,则不需要。” 皮特·亨特


为了控制应用程序的状态,我通常使用Redux。 但是,仅由于Action \ Reducer模型编写最简单的功能的应用程序很费力,并不总是需要使用它。 以普通柜台为例。 在输出中,我想获得一个简单实用的解决方案,该解决方案将使我们能够描述状态模型以及一些更改状态模型的方法,如下所示:


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

MobX可以立即提供这种解决方案,为什么不使用它呢? 与MobX一起工作了一段时间后,我得出的结论是,与可变状态(如MobX)的逻辑相比,我个人更容易使用一系列不可变状态(如Redux)进行操作,而且我不认为内部厨房简单。


总的来说,我想找到一个基于抗扰度的简单状态管理解决方案,该解决方案能够在Angular \ React中使用它并在TypeScript上实现。 快速回顾一下github的开放空间并没有给出合适的解决方案,因此让我们以RxJS / Immer尝试自己制作一个。


我们使用RxJS


我们将以BehaviorSubjet作为基础,它将模拟状态变化流{value: 0} -> {value: 1} -> {value: 2} ,并且还具有可用来获取当前状态的getValue方法。 如果您将BehaviorSubject API与Redux进行比较


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

您可能会注意到它们非常相似。 主要区别在于,在BehaviorSubject可以使用next()方法代替Action/Reducer来设置新状态。


对于上面的计数器示例,实现可能如下所示:


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非常不同。 state.value += 1


添加Immer


为了简化免疫状态的变化,我们将使用Immer库。 Immer允许您由于电流的突变而创建新的不可变状态。 它是这样工作的:


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

行为和主体的捆绑


在我们自己的类中包装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) } } 

我们在其帮助下实现了stor:


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) } 

柜台


故事的最终版本。 为了演示,通过传递setValue参数和setValue异步性,添加一些decrease逻辑,以及其他两种方法:


 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) } } 

与Angular一起使用


由于RxJS是生成的堆栈的基础,因此可以将Angular与async管道结合使用:


 <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/zh-CN483526/


All Articles