I'm new to TCA (The Composable Architecture) so I may have the entirely wrong approach here, so apologies if so I and I would appreciate getting pointed in the right direction. It's a little bit tricky to find resources for this since I've adopted the new ReducerProtocol
approach to TCA and the vast majority of the tutorials/documentation I can find uses the old approach.
Essentially imagine I have two reducers, TodoListFeature and TodoItemFeature. The former displays a list of todo items, and the latter displays a single todo item, and allows editing of its information (e.g. title).
So TodoListFeature
might look like this:
struct TodoListFeature: ReducerProtocol {
struct State {
var todoItems: [TodoItem]
}
enum Action {
case onAppear
case dataLoaded(Result<[TodoItem], Error>)
}
func reduce(into state: inout State, action: Action) -> EffectTask<Action> {
switch action {
case .onAppear:
// perform async action to fetch todos, then dispatch .dataLoaded
case .dataLoaded(let result):
// update state
}
}
}
and then TodoItemFeature
something like this:
struct TodoItemFeature: ReducerProtocol {
struct State {
var saveError: Error?
}
enum Action {
case updatedItem(TodoItem)
}
func reduce(into state: inout State, action: Action) -> EffectTask<Action> {
switch action {
case .updateItem(let item):
// perform async action to fetch todos, then dispatch .dataLoaded
// But then what?
}
}
}
So this is the question - see the "But then what" comment? Essentially I now want to take that updated item, and update my state in TodoListFeature
- in other words, I'd like to update the list.
I've read about composition, but this just seems to be arbitrarily grouping together features into a single store and then scoping that single store depending on the view we're using. How do I get reducers to talk to one another?
Since TCA allows unidirectional data flow, you can always access the child state from the parent reducer. But to answer your first question which is how to make the reducers talk with each other, you should start with integrating the child reducer into the parent domain. Here I updated your code to provide communication between the reducers:
In this example, we first render TodoListView and call the onAppear action when the view appeared. We run an async task to simulate fetching todos from some external storage after 2 seconds and render each todos in a list. Here you can also handle loading state and possible errors that can occur while fetching the data. When you tap a todo item from the list, it displays a sheet with editing feature of the corresponding todo. To inform the parent reducer that a todo is updated, we have the saveTapped enum case action which takes a todoItem parameter (or anything that you want to update) and we call this action once save button is tapped. At that point we can listen to the call of this action from the parent reducer with the help of todoItem presentation action and make the required changes here.
Although this approach totally works and it is fine for the smaller applications, in larger applications, this way can easily lead to complicated code and hard-to-understand bugs. The reason is Enums in Swift does not have any access control and you can easily go trough non-exhaustive integration of the enums once your actions get extended.
Let me explain what I'm trying to say here with an example. Let's say you or some other developer wanted to add some more actions to your TodoItemFeature reducer, such as deleting a todo, and you forgot to implement it in the parent reducer while developing (which would be a very basic mistake but let us assume this is the case for now ). In this case you will not be getting a compile time error or warning, since you already implemented .todoItem case in your reducer. And believe me when the app gets larger, it is quite easy to miss such side effects if you are not getting a compile time error. This is why we want to utilize compile time errors as much as possible.
To prevent such mistakes, we can leverage from the Delegate pattern who is our old friend that we generally come across in UIKit applications. Here I updated the reducers and TodoItemView's toolbar with delegate actions.
With this implementation, if you modify the delegate actions from the child reducer, the app will not compile until you implement that change in the parent domain as well which prevents possible bugs to happen in your application.
If you want, you can further improve your Action enum by splitting internal and ui actions as well. I would recommend you to have a look at this very nice article written by Krzysztof Zabłocki about the action boundaries in TCA and the best practices to get a good grasp of TCA and unidirectional data flow.