I have a TabView with childs (Home, Example, Test) with Scopes to their feature reducer (like in a examples of TCA), and when I clicked on "Home" I launched some Effect.task onAppear{} which execute some downloading from server and save data to State.
Is there any solution how to make that request only once? Because every time I clicked on Home tab, it fires the request, which is already downloaded (Yeah, later I will add swipe to refresh)... Like I'm thinking about check if data is in state and then do not launch that Effect, but I don't know if it is good and reusable solution.
Maybe something like this? (in View I'm using onAppear and onDisappear functions)
struct HomeFeature: ReducerProtocol {
let test = test()
struct State: Equatable {
var home: String?
}
enum Action: Equatable {
case onAppear
case onDisappear
case didLoad(String)
case didFailLoad
}
private enum TestRequestId {}
func reduce(into state: inout State, action: Action) -> EffectTask<Action> {
switch action {
case .onAppear:
return EffectTask.task {
guard let home != nil else {
return .none
}
let result = await asyncResult(for: test)
switch result {
case .success(let test):
return .didLoad(test)
case .failure:
return .didFailLoad
}
}.cancellable(id: TestRequestId.self)
case .onDisappear:
return .cancel(id: TestRequestId.self)
case .didFailLoad:
return .none
case let .didLoad(test):
state.home = test
return .none
}
}
}
Yep, @otondin's solution will work out nicely, and you could even bake it into a dedicated ViewModifier, probably called onFirstAppear. You could even take the extra steps to make it async and handle cancelation so that it behaves like the .task view modifier. That would allow you to start an effect when the feature first appears and automatically tear it down when the feature goes away.
Hey @otondin, unfortunately that's not correct because it will cancel the task the first time it disappears rather than the last time it disappears.
Honestly I forgot how tricky and nuanced it is to have an onFirstAppear that works like .task. You need to track an object in the lifecycle of the view so you can detect when it deallocates, and that represents the final "disappear" of the view. And I think there are some bugs in SwiftUI that prevent such objects from being deallocated at the right time.
So if the non-async version of onFirstAppear works for your use case, that may be the way to go for now.