"Sie werden verstehen, wann Sie Flux brauchen. Wenn Sie nicht sicher sind, ob Sie es brauchen, brauchen Sie es nicht." Pete jagen

Um den Status der Anwendung zu kontrollieren, verwende ich normalerweise Redux. Es ist jedoch nicht immer erforderlich, das Action \ Reducer-Modell zu verwenden, schon allein aufgrund der mühsamen Anwendung zum Schreiben der einfachsten Funktion. Nehmen Sie einen gewöhnlichen Zähler als Beispiel. Am Ende wollte ich eine einfache und praktische Lösung finden, die es uns ermöglicht, das Zustandsmodell und einige Methoden zu beschreiben, die es ändern:
state = {value: 0} increase() { state.value += 1 } decrease() { state.value -= 1 }
Es scheint, als könne MobX eine solche Lösung anbieten. Warum also nicht? Nachdem ich eine Weile mit MobX gearbeitet hatte, kam ich zu dem Schluss, dass es für mich persönlich einfacher ist, mit einer Sequenz unveränderlicher Zustände (wie Redux) als mit der Logik eines veränderlichen Zustands (wie MobX) zu arbeiten, und ich würde seine interne Küche nicht einfach nennen.
Im Allgemeinen wollte ich eine einfache, auf Immunität basierende Zustandsverwaltungslösung finden, die in Angular \ React verwendet und in TypeScript implementiert werden kann. Ein kurzer Rückblick auf Github-Freiflächen ergab keine geeignete Lösung. Nehmen wir also RxJS / Immer und versuchen Sie, unsere eigene Lösung zu finden.
Wir verwenden RxJS
Wir werden BehaviorSubjet
als Basis nehmen, das den Fluss von Statusänderungen modelliert {value: 0} -> {value: 1} -> {value: 2}
und das auch eine getValue
Methode hat, mit der Sie den aktuellen Status getValue
können. Wenn Sie die BehaviorSubject
API mit Redux vergleichen
getValue() / getState() //
subscribe() / subscribe() //
next(value) / dispatch(action), replaceReducer(nextReducer) //
Möglicherweise stellen Sie fest, dass sie sich sehr ähnlich sind. Der Hauptunterschied besteht darin, dass in BehaviorSubject
anstelle von Action/Reducer
neue Status mithilfe der next()
-Methode festgelegt werden kann.
Für das obige Zählerbeispiel könnte eine Implementierung folgendermaßen aussehen:
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
die Redundanz von Wiederholungen aus this.state.next
und die Umständlichkeit von Zustandsänderungen. Dies state.value += 1
stark vom gewünschten Ergebniszustand state.value += 1
Immer hinzufügen
Um die Änderung des Immunstatus zu vereinfachen, verwenden wir die Immer-Bibliothek. Immer ermöglicht es Ihnen, einen neuen unveränderlichen Zustand aufgrund der Mutation des Stroms zu erstellen. Es funktioniert so:
const state = {value: 0}
Bündel von BehaviorSubject und Immer
Binden Sie die Verwendung von BehaviorSubject
und Immer in unsere eigene Klasse ein und nennen Sie sie 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
Mit RxState
schreiben wir RxState
neu:
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 }) } }
Diff - 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 + }) }
Es sieht ein bisschen besser aus als die erste Option, aber es besteht immer noch die Notwendigkeit, jedes Mal updateState
. Um dieses Problem zu lösen, erstellen Sie eine weitere Klasse und nennen Sie sie SimpleImmutableStore
. Sie ist die Basis für die Story.
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) } }
Wir implementieren den Stor mit seiner Hilfe:
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 }) } }
Diff -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 }) } }
Wie Sie sehen, hat sich nichts wesentlich geändert, aber jetzt haben alle Methoden einen gemeinsamen Code in Form eines Wrappers this.updateState
. Um diese Duplizierung zu updateState
, schreiben wir eine Funktion, die alle Klassenmethoden in einem updateState
Aufruf updateState
:
const wrapped = Symbol()
und wir werden es im Konstruktor aufrufen (falls gewünscht, kann diese Methode auch als Dekorator für die Klasse implementiert werden)
constructor(initialState: TState ) { this.rxState = new RxState<TState>(initialState) wrapMethodsWithUpdateState(this.constructor) }
Counterstore
Die endgültige Version der Geschichte. setValue
zur Demonstration eine kleine Logik zum decrease
und einige weitere Methoden hinzu, indem Sie den Parameter setValue
und setValue
Asynchronität von 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) } }
Verwenden Sie mit Angular
Da RxJS die Basis des resultierenden Stacks ist, kann es mit Angular in Verbindung mit einer async
Pipe verwendet werden:
<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>
Mit React verwenden
Für React schreiben wir einen benutzerdefinierten Hook:
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 }
Komponente
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> ) }
Fazit
Das Ergebnis ist eine relativ einfache und funktionale Lösung, die ich regelmäßig in meinen Projekten verwende. Wenn Sie möchten, können Sie dieser Seite verschiedene nützliche Funktionen hinzufügen: Middleware, Status-Slicing, Updaterollback - dies würde jedoch den Rahmen dieses Artikels sprengen. Das Ergebnis solcher Ergänzungen kann auf dem Github https://github.com/simmor-store/simmor gefunden werden
Für Anregungen und Kommentare wäre ich dankbar.