There is a common pattern of using actions to share logic across multiple parts of a reducer. This is inefficient. Sending actions is not as lightweight as calling a method on a class.
Suppose you want to run shared logic after three different actions:
@Reducerstruct Feature { var body: some Reducer<State, Action> { Reduce { state, action in switch action { case .buttonTapped: state.count += 1 return .send(.sharedComputation) case .toggleChanged: state.isEnabled.toggle() return .send(.sharedComputation) case let .textFieldChanged(text): state.description = text return .send(.sharedComputation) case .sharedComputation: // Some shared work return .run { send in // A shared effect } } } }}
This sends two actions for every user action, which is inefficient.
Return an effect to perform work in the cooperative thread pool:
case .buttonTapped: return .run { send in var result = // ... for (index, value) in someLargeCollection.enumerated() { // Some intense computation with value // Yield every once in a while to cooperate in the thread pool if index.isMultiple(of: 1_000) { await Task.yield() } } await send(.computationResponse(result)) }case let .computationResponse(result): state.result = result
Sprinkle in Task.yield() periodically to avoid blocking threads in the cooperative pool.
case .startButtonTapped: return .run { send in var count = 0 let max = await self.eventsClient.count() for await event in self.eventsClient.events() { defer { count += 1 } await send(.progress(Double(count) / Double(max))) // ❌ Too many! } }
Report it periodically:
case .startButtonTapped: return .run { send in var count = 0 let max = await self.eventsClient.count() let interval = max / 100 // Report at most 100 times for await event in self.eventsClient.events() { defer { count += 1 } if count.isMultiple(of: interval) { await send(.progress(Double(count) / Double(max))) // ✅ Much better! } } }