I wonder if it's an established practice that endpoints in environment must return Effect type?
Has anyone used environment with simple function that returns a value outside of the state?
To illustrate consider these environments:
struct EffectfulEnvironment {
var endpoint: () -> Effect<String,Never>
}
versus
struct PlainEnvironment {
var endpoint: () -> String
}
And think of endpoint as a function returning, say, access token to web api from a keychain.
Will I have any negative side effects by using PlainEnvironment, calling endpoint() directly in reducer, mutating state and then returning effect with action?
We think it's perfectly fine to control synchronous effects in this way and in fact we prefer it! Effects like the current date, random numbers (or even random isowords puzzles) can be controlled in the environment and returned as synchronous data and probably should be!
The strict alternative gets messy quickly, as you need to introduce an actions just to "receive" this data back into the reducer:
struct MyState {
var currentDate: Date?
var randomNumber: Int?
}
enum MyAction {
case onAppear
case setCurrentDate(Date)
case setRandomNumber(Int)
}
struct MyEnvironment {
var date: () -> Effect<Date>
var randomNumber: () -> Effect<Int>
}
let myReducer = Reducer<
MyState, MyAction, MyEnvironment
> { state, action, environment in
switch action {
case .onAppear:
return .merge(
environment.date().map(MyAction.setCurrentDate),
environment.randomNumber().map(MyAction.setRandomNumber)
)
case let .setCurrentDate(date):
state.currentDate = date
return .none
case let .setRandomNumber(number):
state.randomNumber = number
return.none
}
Synchronous control eliminates the need for a lot of this extra "shuffling" of work:
struct MyState {
var currentDate: Date?
var randomNumber: Int?
}
enum MyAction {
case onAppear
- case setCurrentDate(Date)
- case setRandomNumber(Int)
}
struct MyEnvironment {
- var date: () -> Effect<Date>
+ var date: () -> Date
- var randomNumber: () -> Effect<Int>
+ var randomNumber: () -> Int
}
let myReducer = Reducer<
MyState, MyAction, MyEnvironment
> { state, action, environment in
switch action {
case .onAppear:
+ state.currentDate = environment.date()
+ state.randomNumber = environment.randomNumber()
+ return .none
- return .merge(
- environment.date().map(MyAction.setCurrentDate),
- environment.randomNumber().map(MyAction.setRandomNumber)
- )
- case let .setCurrentDate(date):
- state.currentDate = date
- return .none
- case let .setRandomNumber(number):
- state.randomNumber = number
- return.none
}
@stephencelis just wondering: will the upcoming Swift concurrency additions help bridge the gap between plain vs effectful actions? Or maybe help simplify the way effects can be declared/propagated?
The only issue with synchronous effects is that they will use the main thread and may introduce hitches if they are CPU or IO intensive. In that case returning effects and subscribing to them on the background queue is preferable.