Last active
January 15, 2018 20:08
-
-
Save fellipecaetano/7a343ea7794ddf96025a8ddcc1240e54 to your computer and use it in GitHub Desktop.
Extending Redux with an effect system
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* Credits to @gsampaio for coming up with the Effect<S, A> type. | |
* Even though this flavor of reducer also describes side-effects, | |
* testing is still trivial since it's sufficient to check if the | |
* side-effects are of the right type. The actual execution of the | |
* effect can be tested separately using integration tests. */ | |
import RxSwift | |
class Effect<S: Equatable, A: Action> { | |
open func execute(state: () -> S) -> Observable<A> { | |
fatalError("not implemented") | |
} | |
static func empty<S, A>() -> Effect<S, A> { | |
return EmptyEffect() | |
} | |
private class EmptyEffect<S: Equatable, A: Action>: Effect<S, A> { | |
override func execute(state: () -> S) -> Observable<A> { | |
return .empty() | |
} | |
} | |
} | |
protocol Action {} | |
struct Reducer<S: Equatable, A: Action> { | |
let reduce: (S, A) -> (S, Effect<S, A>) | |
init (_ reduce: @escaping (S, A) -> (S, Effect<S, A>)) { | |
self.reduce = reduce | |
} | |
} | |
class Store<S: Equatable, A: Action> { | |
private let actionSink: AnyObserver<A> | |
private let state: Observable<S> | |
fileprivate init (actionSink: AnyObserver<A>, state: Observable<S>) { | |
self.actionSink = actionSink | |
self.state = state | |
} | |
static func create<S, A>(reducer: Reducer<S, A>, initialState: S) -> Store<S, A> { | |
let actionSink: AnyObserver<A> | |
let stateSubject = BehaviorSubject<S>(value: initialState) | |
var dispatch: (A) -> Void = { _ in } | |
var interpret: (Effect<S, A>) -> Void = { _ in } | |
var currentState = initialState { | |
didSet { stateSubject.onNext(currentState) } | |
} | |
actionSink = AnyObserver<A> { event in | |
switch event { | |
case let .next(action): dispatch(action) | |
default: break | |
} | |
} | |
dispatch = { action in | |
let (nextState, nextEffect) = reducer.reduce(currentState, action) | |
currentState = nextState | |
interpret(nextEffect) | |
} | |
interpret = { effect in | |
_ = effect.execute(state: { currentState }).subscribe(onNext: dispatch) | |
} | |
return Store<S, A>(actionSink: actionSink, state: stateSubject) | |
} | |
func asObserver() -> AnyObserver<A> { | |
return actionSink | |
} | |
func asObservable() -> Observable<S> { | |
return state | |
} | |
} | |
// | |
class PrintEffect: Effect<PrintState, PrintAction> { | |
override func execute(state: () -> PrintState) -> Observable<PrintAction> { | |
print("State before effect:", state()) | |
return .just(.finishPrinting) | |
} | |
} | |
enum PrintAction: Action { | |
case startPrinting | |
case finishPrinting | |
} | |
struct PrintState: Equatable { | |
let isPrinting: Bool | |
static func == (lhs: PrintState, rhs: PrintState) -> Bool { | |
return lhs.isPrinting == rhs.isPrinting | |
} | |
} | |
func PrintReducer() -> Reducer<PrintState, PrintAction> { | |
return Reducer { _, action in | |
switch action { | |
case .startPrinting: | |
return (PrintState(isPrinting: true), PrintEffect()) | |
case .finishPrinting: | |
return (PrintState(isPrinting: false), .empty()) | |
} | |
} | |
} | |
let store = Store<PrintState, PrintAction>.create(reducer: PrintReducer(), initialState: PrintState(isPrinting: false)) | |
_ = store.asObservable().subscribe(onNext: { state in print("Subscribed state:", state) }) | |
store.asObserver().onNext(.startPrinting) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment