How to launch effect onApper only once?

Hey,

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
        }
    }
    
}

Thanks

Hey @Vlados !
I think you'd like to add a hasAppeared @State property within your view, something like this:

struct MyView: View {
    let store: StoreOf<MyFeature>
    @State private var hasAppeared = false

    init(store: StoreOf<MyFeature> {
        self.store = store
    }
    
    var body: some View {
        WithViewStore(store, observe: { $0 }) { viewStore in
               // content
                .onAppear {
                    guard !hasAppeared else { return }
                    hasAppreared = true
                    viewStore.send(.onAppear)
                }
        }
    }
}

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.

2 Likes

@mbrandonw sweet! Do you mean something like:

public struct FirstAppearMofier: ViewModifier {

    private let action: () async -> Void
    @State private var hasAppeared = false
    
    public init(_ action: @escaping () async -> Void) {
        self.action = action
    }
    
    public func body(content: Content) -> some View {
        content
            .task {
                guard !hasAppeared else { return }
                hasAppeared = true
                await action()
            }
    }
}

public extension View {
    
    func onFirstAppear(_ action: @escaping () async -> Void) -> some View {
        modifier(FirstAppearMofier(action))
    }
}
2 Likes

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.

4 Likes

I make modifier like this