State-Driven Side Effects
Related: Software Engineering, iOS App Development
State-driven side effects implementation allows for the elimination of both middleware and sync actions.
Let's start with a SwiftUI example of a simple state with a single boolean value isLoading
and a simple spinner indicator view for it with a button.
struct ContentView: View {
@State private var isLoading = 0.5
var body: some View {
VStack {
ProgressView // visible when `isLoading` is true.
.opacity(isLoading ? 1.0 : 0.0)
Button("Toggle") {
isLoading.toggle()
}
}
}
In SwiftUI the View is a function of state meaning that it's completely defined by the state.
We can imagine that the spinner is actually doing some heavy work, like spinning really hard. The work that the spinner is performing is controlled by the state: isLoading
flag which may turn the spinner on/off or restart it.
From that perspective, UI can be considered a side effect as an interface to the outer world.
The idea of State-Driven side effects suggests that all side effects that the app produces can be implemented as a function of state, just like declarative UI. In that case at any point in time, the amount of async work that our app produces is completely defined by its state. It can be restated, canceled, or delayed.
There is only need to be a diffing mechanism that will compare the work which is expected to be performed with the actual work and will start/restart/cancel it when needed
Here is an example from Puredux documentation:
// Add effect state to the state
struct AppState {
private(set) var theJob: Effect.State = .idle()
}
// Add related actions
enum Action {
case jobSuccess(SomeResult)
case startJob
case cancelJob
case jobFailure(Error)
}
// Handle actions in the reducer
extension AppState {
mutating func reduce(_ action: Action) {
switch action {
case .jobSuccess:
theJob.succeed()
case .startJob:
theJob.run()
case .cancelJob:
theJob.cancel()
case .jobFailure(let error):
theJob.retryOrFailWith(error)
}
}
}
// Add SideEffect to the store in a declarative way
let store = StateStore<AppState, Action>(AppState()) { state, action in
state.reduce(action)
}
.effect(\.theJob, on: .main) { appState, dispatch in
Effect {
do {
let result = try await apiService.fetch()
dispatch(.jobSuccess(result))
} catch {
dispatch(.jobFailure(error))
}
}
}
store.dispatch(.startJob)
Comments