Environment without effects

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
 }
2 Likes

Thanks for reply, @stephencelis! I had a hunch about messyness with extra actions just to receive the data back. I guess I just had to sleep on it :)

@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.

1 Like