Skip to main content

Effects

Effects represent side effects in TCA. They allow reducers to interact with the outside world, such as making API calls, reading from disk, or starting timers, while keeping the reducer logic pure and testable.

The Effect Type

The Effect type is a wrapper around asynchronous operations that can emit actions:
Effect.swift
public struct Effect<Action>: Sendable {
  enum Operation: Sendable {
    case none
    case publisher(AnyPublisher<Action, Never>)
    case run(
      name: String? = nil,
      priority: TaskPriority? = nil,
      operation: @Sendable (_ send: Send<Action>) async -> Void
    )
  }
}
Source: Effect.swift:5-24

Creating Effects

Effect.none

When no side effect is needed:
case .cancelButtonTapped:
  state.isLoading = false
  return .none
Source: Effect.swift:46-49

Effect.run

The primary way to create effects with async/await:
case .fetchButtonTapped:
  state.isLoading = true
  return .run { send in
    do {
      let data = try await apiClient.fetchData()
      await send(.dataLoaded(data))
    } catch {
      await send(.dataFailed(error))
    }
  }
Source: Effect.swift:92-136
Effect.run automatically captures dependencies and provides a send function for emitting actions back into the system.

Effect.send

Immediately emit a single action:
case .delegate(.didComplete):
  // Notify parent immediately
  return .send(.delegate(.childDidComplete))
Source: Effect.swift:138-149
Avoid using Effect.send to share logic between actions. Instead, extract shared logic into helper functions or use proper action composition.

The Send Type

The Send type allows effects to emit actions back into the system:
Effect.swift
@MainActor
public struct Send<Action>: Sendable {
  public func callAsFunction(_ action: Action)
  public func callAsFunction(_ action: Action, animation: Animation?)
  public func callAsFunction(_ action: Action, transaction: Transaction)
}
Source: Effect.swift:196-231

Using Send

return .run { send in
  // Send an action
  await send(.started)
  
  // Send with animation
  await send(.updated, animation: .spring())
  
  // Send with transaction
  await send(.completed, transaction: Transaction(animation: .default))
}
Send implements callAsFunction, so you call it like a function: send(.action) instead of send.send(.action).

Effect Patterns

Async Sequences

Stream values from async sequences:
case .startListening:
  return .run { send in
    for await event in eventsClient.stream() {
      await send(.eventReceived(event))
    }
  }

Long-Running Effects

case .startTimer:
  return .run { send in
    while !Task.isCancelled {
      try await Task.sleep(for: .seconds(1))
      await send(.tick)
    }
  }

Error Handling

Use the catch parameter to handle errors:
case .fetch:
  return .run { send in
    let data = try await apiClient.fetchData()
    await send(.dataLoaded(data))
  } catch: { error, send in
    await send(.errorOccurred(error))
  }

Task Priority

Specify priority for effects:
return .run(priority: .background) { send in
  // Low-priority background work
  let result = await heavyComputation()
  await send(.completed(result))
}

Combining Effects

Merge

Run multiple effects concurrently:
case .onAppear:
  return .merge(
    .run { send in
      let user = try await apiClient.fetchUser()
      await send(.userLoaded(user))
    },
    .run { send in
      let settings = try await apiClient.fetchSettings()
      await send(.settingsLoaded(settings))
    }
  )
Source: Effect.swift:236-294

Concatenate

Run effects sequentially:
case .startFlow:
  return .concatenate(
    .run { send in
      await send(.step1Started)
      try await Task.sleep(for: .seconds(1))
      await send(.step1Completed)
    },
    .run { send in
      await send(.step2Started)
      try await Task.sleep(for: .seconds(1))
      await send(.step2Completed)
    }
  )
Source: Effect.swift:296-361

Cancellation

Effects are automatically cancelled when:
  • The store is deallocated
  • The view disappears (if using .task { await store.send(.task).finish() })
  • You explicitly cancel them

Manual Cancellation

Use cancellation IDs to manually cancel effects:
private enum CancelID { case timer }

var body: some Reducer<State, Action> {
  Reduce { state, action in
    switch action {
    case .startTimer:
      return .run { send in
        while !Task.isCancelled {
          try await Task.sleep(for: .seconds(1))
          await send(.tick)
        }
      }
      .cancellable(id: CancelID.timer)
      
    case .stopTimer:
      return .cancel(id: CancelID.timer)
    }
  }
}

Cancel All Effects

case .logoutButtonTapped:
  return .cancel(ids: CancelID.allCases)

Testing Effects

Exhaustive Testing

TestStore requires you to assert on all effects:
@MainActor
func testFetch() async {
  let store = TestStore(initialState: Feature.State()) {
    Feature()
  } withDependencies: {
    $0.apiClient.fetchData = { "Test data" }
  }
  
  await store.send(.fetchButtonTapped) {
    $0.isLoading = true
  }
  
  // Assert on the effect's action
  await store.receive(\.dataLoaded) {
    $0.isLoading = false
    $0.data = "Test data"
  }
}

Non-Exhaustive Testing

For flexibility in tests:
let store = TestStore(initialState: Feature.State()) {
  Feature()
} withDependencies: {
  $0.apiClient.fetchData = { "Test data" }
}

store.exhaustivity = .off

await store.send(.fetchButtonTapped)
// Don't need to assert on received actions

Debouncing and Throttling

Debounce

Delay effect execution until input stops:
case let .searchQueryChanged(query):
  state.searchQuery = query
  return .run { send in
    try await Task.sleep(for: .milliseconds(300))
    await send(.performSearch)
  }
  .debounce(id: CancelID.search, for: .milliseconds(300), scheduler: DispatchQueue.main)

Throttle

Limit effect execution frequency:
case .buttonTapped:
  return .run { send in
    await send(.performAction)
  }
  .throttle(id: CancelID.action, for: .seconds(1), scheduler: DispatchQueue.main, latest: true)

Effect Animations

Send actions with animations:
case .toggleButtonTapped:
  return .run { send in
    await send(.toggle, animation: .spring())
  }
case .toggle:
  state.isExpanded.toggle()
  return .none

EffectOf Type Alias

Use EffectOf for less verbose type signatures:
Effect.swift
public typealias EffectOf<R: Reducer> = Effect<R.Action>
Source: Effect.swift:39
// Instead of:
func myEffect() -> Effect<Feature.Action> { /* ... */ }

// You can write:
func myEffect() -> EffectOf<Feature> { /* ... */ }

Best Practices

1

Keep Effects Focused

Each effect should do one thing. Use .merge() to combine multiple focused effects:
return .merge(
  .run { /* fetch user */ },
  .run { /* fetch settings */ },
  .run { /* start analytics */ }
)
2

Always Handle Errors

Use the catch parameter or do-catch blocks:
return .run { send in
  do {
    let data = try await apiClient.fetch()
    await send(.success(data))
  } catch {
    await send(.failure(error))
  }
}
3

Use Cancellation IDs

Always provide cancellation IDs for long-running effects:
return .run { /* ... */ }
  .cancellable(id: CancelID.network)
4

Test All Effects

Use TestStore to verify effects emit the expected actions:
await store.send(.fetch)
await store.receive(\.dataLoaded)
5

Check for Cancellation

Respect cancellation in long-running effects:
return .run { send in
  while !Task.isCancelled {
    // Do work
  }
}

Common Pitfalls

Escaping Send: Don’t escape the send function from Effect.run:
// ❌ Don't do this
return .run { send in
  Task.detached {
    await send(.action)  // May be called after effect completes
  }
}

// ✅ Do this instead
return .run { send in
  await Task.detached {
    return await performWork()
  }.value
  await send(.action)
}
Unhandled Errors: Effects that throw without a catch handler will trigger runtime warnings:
// ❌ Unhandled error
return .run { send in
  try await riskyOperation()  // ⚠️ Error not handled
}

// ✅ Handle the error
return .run { send in
  try await riskyOperation()
} catch: { error, send in
  await send(.errorOccurred(error))
}
  • Reducers - Where effects are returned
  • Testing - Testing effects with TestStore
  • Dependencies - Injecting dependencies into effects
  • Store - How effects are executed

Build docs developers (and LLMs) love