Hi, I'm trying to understand the implications of TCA's concurrency update on existing code (demonstrated here with TCA's Todo example). If we were dealing with an async UUID-client like
struct AppEnvironment {
var mainQueue: AnySchedulerOf<DispatchQueue>
var uuid: @Sendable () async -> UUID
}
then the addTodoButtonTapped case inside the reducer could read like
case .addTodoButtonTapped:
return .task { [state] in
await state.todos.insert(Todo(id: environment.uuid()), at: 0)
return .sortCompletedTodos
}
resulting in an error
Cannot use mutating member on immutable value: 'state' is an immutable capture
What is the proper way to handle this? Am I supposed to provide actions with the sole purpose of mutating state?
case .addTodoButtonTapped:
return .task { [state] in
let todo = await Todo(id: environment.uuid())
return .updateTodos(todo)
}
case let .updateTodos(todo):
state.todos.insert(todo, at: 0)
return .none
The composable architecture intentionally does not allow you to mutate app state in effects. The purpose of effects are to handle the "edges" of your program where you don't have control over the results, for example, disk writes, network calls, user input, etc. Effects are designed to handle these sort of fail-able events and hand concrete actions back to your reducer to manage the results. You then update your state based on the actions returned from your effects.
So yes, you are supposed to provide actions with the sole purpose of mutating state as you did in your last snippet.
Ok, but the original example in the repository mutates the state in every action inside the reducer!?
Because it is a simple example only one action uses an environment with a non-async endpoint uuid(). I simply modified this to async, which leads to my question: How do we update state out of an async context?
The modified Todo app with the additional state mutating action runs just fine, but I was hoping that there is a simpler way (like without concurrency). Imagine a large application with a database with multiple tables inside the environment, all database endpoints async... That could be a ton of additional actions, which are not necessary without TCA's concurrency.
Ah, it was not clear to me that you had pulled the example from the TCA repo. I had missed the part in parenthesis were you explicitly said that .
Modifying state in the reducer is the way it was designed. The reducer returns two things: a new state and an effect. The effect is returned directly with thereturn keyword. The state is modified in place using inout. The end result is, in essence, the same as returning the tuple (State, Effect) from the reducer. Just don't modify the state in the effect that is returned from the reducer as that won't execute until later and your state mutations will get out of sync.
It's perfectly reasonable for fast dependencies like uuid and randomNumber to be synchronous and callable from inside the reducer to avoid the need to define extra actions.
It's possible that even a database (like user defaults) could be queried synchronously in the reducer!
However, if a database is may be slow to access or can fail with errors, it's best to push into an effect.
All the examples in the TCA repo are kept up-to-date with each release, so the synchronicity of uuid is intentional, and not an accidental omission from the update:
As a note, this isn't even possible due to how inout works in Swift, which is quite nice
Attempting to mutate state in an async context will fail to compile.
Sorry, it it clear to me that all examples are up-to-date. I just modified it as an illustration to my question. I would not use async here
I'm working on a larger app with a database environment where I found a lot of code which, after converting to async, needed additional actions. So may be I have to restrict myself to use async/await here.