Skip to content

[Feature Request] Allow keeping Atoms alive within a scope until it's deallocation #165

@strangeliu

Description

@strangeliu

Checklist

  • Reviewed the README and documentation.
  • Checked existing issues & PRs to ensure not duplicated.

Description

Currently, atoms in a scope are released when there are no active subscriptions, even if the scope itself still exists. This proposal suggests adding a mechanism to retain atoms within a scope until the scope is explicitly destroyed, which is particularly useful for state preservation in lazy-loaded containers (e.g., scrollable lists).

Example Use Case

In a LazyVStack with reusable cells (like ScopeCellView below), we want to preserve fetched data even when cells scroll offscreen. Atoms should only be released when their parent container (AtomScope) is dismissed:

struct ContentView: View {
    
    var body: some View {
        NavigationStack {
            List {
                NavigationLink("Scope test") {
                    ScopeTestView()
                }
            }
        }
    }
}

// Parent container
struct ScopeTestView: View {
    var body: some View {
        AtomScope(id: "lazyContainer") {
            ScrollView {
                LazyVStack {
                    ForEach(0..<20) { num in
                        ScopeCellView(num: num) // Cells may be reused/destroyed
                    }
                }
            }
        }
    }
}

// Child view
struct ScopeCellView: View {
    
    let num: Int
    
    @ViewContext
    private var viewContext
    
    private var value: AsyncPhase<Int, Error> {
        viewContext.watch(NumberTestAtom(number: num))
    }
    
    var body: some View {
        HStack {
            Text(num.description)
            Spacer()
            switch value {
            case .suspending:
                ProgressView()
            case .success(let value):
                Text(value.description)
            case .failure(let error):
                Text(error.localizedDescription)
            }
        }
    }
}

// Atom definition (current workaround)
struct NumberTestAtom: AsyncPhaseAtom, Scoped, KeepAlive {
    let number: Int
    
    func value(context: Context) async throws -> Int {
        try await Task.sleep(nanoseconds: 3_000_000_000) // Costly fetch
        return number
    }
    
    var scopeID: String { "lazyContainer" } // Scoped to parent container
}

Alternative Solution

No response

Proposed Solution

  1. Maybe we should introduce a LifecycleScope for Atom lifetime management, since the existing AtomScope is used for data isolation.
  2. introduce a new Atom attribute named KeepAliveInScopes
protocol KeepAliveInScopes {
    
    var aliveInScopes: [LifecycleScopeID] { get }
}

Motivation & Context

  1. Performance Optimization: Avoid redundant async operations when revisiting scoped elements (e.g., list items).
  2. Expected Behavior Alignment: Scoped atoms should logically belong to their parent scope's lifecycle, not individual view subscriptions.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions