combine-reactive

Combine Reactive — Expert Decisions

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "combine-reactive" with this command: npx skills add kaakati/rails-enterprise-dev/kaakati-rails-enterprise-dev-combine-reactive

Combine Reactive — Expert Decisions

Expert decision frameworks for Combine choices. Claude knows Combine syntax — this skill provides judgment calls for when Combine adds value vs async/await and how to avoid common pitfalls.

Decision Trees

Combine vs Async/Await

What's your data flow pattern? ├─ Single async operation (fetch once) │ └─ async/await │ Simpler, built into language │ ├─ Stream of values over time │ └─ Is it UI-driven (user events)? │ ├─ YES → Combine (debounce, throttle shine here) │ └─ NO → AsyncSequence may suffice │ ├─ Combining multiple data sources │ └─ How many sources? │ ├─ 2-3 → Combine's CombineLatest, Zip │ └─ Many → Consider structured concurrency (TaskGroup) │ └─ Existing codebase uses Combine? └─ Maintain consistency unless migrating

The trap: Using Combine for simple one-shot async. async/await is cleaner and doesn't need cancellable management.

Subject Type Selection

Do you need to access current value? ├─ YES → CurrentValueSubject │ Can read .value synchronously │ New subscribers get current value immediately │ └─ NO → Is it event-based (discrete occurrences)? ├─ YES → PassthroughSubject │ No stored value, subscribers only get new emissions │ └─ NO (need replay of past values) → Consider external state management Combine has no built-in ReplaySubject

Operator Chain Design

What transformation is needed? ├─ Value transformation │ └─ map (simple), compactMap (filter nils), flatMap (nested publishers) │ ├─ Filtering │ └─ filter, removeDuplicates, first, dropFirst │ ├─ Timing │ └─ User input → debounce (wait for pause) │ Scrolling → throttle (rate limit) │ ├─ Error handling │ └─ Can provide fallback? → catch, replaceError │ Need retry? → retry(n) │ └─ Combining streams └─ Need all values paired? → zip Need latest from each? → combineLatest Merge into one stream? → merge

NEVER Do

Subscription Management

NEVER forget to store subscriptions:

// ❌ Subscription cancelled immediately func loadData() { publisher.sink { value in self.data = value // Never called! } // sink returns AnyCancellable that's immediately deallocated }

// ✅ Store in Set<AnyCancellable> private var cancellables = Set<AnyCancellable>()

func loadData() { publisher.sink { value in self.data = value } .store(in: &cancellables) }

NEVER capture self strongly in sink closures:

// ❌ Retain cycle — ViewModel never deallocates publisher .sink { value in self.updateUI(value) // Strong capture! } .store(in: &cancellables)

// ✅ Use [weak self] publisher .sink { [weak self] value in self?.updateUI(value) } .store(in: &cancellables)

NEVER use assign(to:on:) with classes:

// ❌ Strong reference to self — retain cycle publisher .assign(to: .text, on: self) // Retains self! .store(in: &cancellables)

// ✅ Use sink with weak self publisher .sink { [weak self] value in self?.text = value } .store(in: &cancellables)

// Or use assign(to:) with @Published (no retain cycle) publisher .assign(to: &$text) // Safe — doesn't return cancellable

Subject Misuse

NEVER expose subjects directly:

// ❌ External code can send values class ViewModel { let users = PassthroughSubject<[User], Never>() // Public subject! }

// External code: viewModel.users.send([]) // Breaks encapsulation

// ✅ Expose as Publisher class ViewModel { private let usersSubject = PassthroughSubject<[User], Never>()

var users: AnyPublisher&#x3C;[User], Never> {
    usersSubject.eraseToAnyPublisher()
}

}

NEVER send on subject from multiple threads without care:

// ❌ Race condition — undefined behavior DispatchQueue.global().async { subject.send(value1) } DispatchQueue.global().async { subject.send(value2) }

// ✅ Serialize access let subject = PassthroughSubject<Value, Never>() let serialQueue = DispatchQueue(label: "subject.serial")

serialQueue.async { subject.send(value) }

// Or use .receive(on:) to ensure delivery on specific scheduler

Operator Mistakes

NEVER use flatMap when you mean map:

// ❌ Confusing — flatMap for non-publisher transformation publisher .flatMap { user -> AnyPublisher<String, Never> in Just(user.name).eraseToAnyPublisher() // Overkill! }

// ✅ Use map for simple transformation publisher .map { user in user.name }

NEVER forget to handle errors in sink:

// ❌ Compiler allows this but errors are ignored publisher // Has Error type .sink { value in // Only handles values, error terminates silently }

// ✅ Handle both completion and value publisher .sink( receiveCompletion: { completion in if case .failure(let error) = completion { handleError(error) } }, receiveValue: { value in handleValue(value) } )

NEVER debounce without scheduler on main:

// ❌ Debouncing on background scheduler, updating UI searchText .debounce(for: .milliseconds(300), scheduler: DispatchQueue.global()) .sink { [weak self] text in self?.results = search(text) // UI update on background thread! }

// ✅ Receive on main for UI updates searchText .debounce(for: .milliseconds(300), scheduler: DispatchQueue.main) .sink { [weak self] text in self?.results = search(text) }

// Or explicitly receive on main searchText .debounce(for: .milliseconds(300), scheduler: DispatchQueue.global()) .receive(on: DispatchQueue.main) .sink { ... }

Essential Patterns

ViewModel with Combine

@MainActor final class SearchViewModel: ObservableObject { @Published var searchText = "" @Published private(set) var results: [SearchResult] = [] @Published private(set) var isLoading = false @Published private(set) var error: Error?

private let searchService: SearchServiceProtocol
private var cancellables = Set&#x3C;AnyCancellable>()

init(searchService: SearchServiceProtocol) {
    self.searchService = searchService
    setupSearch()
}

private func setupSearch() {
    $searchText
        .debounce(for: .milliseconds(300), scheduler: DispatchQueue.main)
        .removeDuplicates()
        .filter { !$0.isEmpty }
        .handleEvents(receiveOutput: { [weak self] _ in
            self?.isLoading = true
            self?.error = nil
        })
        .flatMap { [weak self] query -> AnyPublisher&#x3C;[SearchResult], Never> in
            guard let self = self else {
                return Just([]).eraseToAnyPublisher()
            }
            return self.searchService.search(query: query)
                .catch { [weak self] error -> Just&#x3C;[SearchResult]> in
                    self?.error = error
                    return Just([])
                }
                .eraseToAnyPublisher()
        }
        .receive(on: DispatchQueue.main)
        .sink { [weak self] results in
            self?.results = results
            self?.isLoading = false
        }
        .store(in: &#x26;cancellables)
}

}

Combine to Async Bridge

extension Publisher where Failure == Error { func async() async throws -> Output { try await withCheckedThrowingContinuation { continuation in var cancellable: AnyCancellable? var didResume = false

        cancellable = first()
            .sink(
                receiveCompletion: { completion in
                    guard !didResume else { return }
                    didResume = true

                    switch completion {
                    case .finished:
                        break  // Value handled in receiveValue
                    case .failure(let error):
                        continuation.resume(throwing: error)
                    }
                    cancellable?.cancel()
                },
                receiveValue: { value in
                    guard !didResume else { return }
                    didResume = true
                    continuation.resume(returning: value)
                }
            )
    }
}

}

// Usage Task { let user = try await fetchUserPublisher.async() }

Event Bus Pattern

final class EventBus { static let shared = EventBus()

// Private subjects
private let userLoggedInSubject = PassthroughSubject&#x3C;User, Never>()
private let userLoggedOutSubject = PassthroughSubject&#x3C;Void, Never>()
private let cartUpdatedSubject = PassthroughSubject&#x3C;Cart, Never>()

// Public publishers (read-only)
var userLoggedIn: AnyPublisher&#x3C;User, Never> {
    userLoggedInSubject.eraseToAnyPublisher()
}

var userLoggedOut: AnyPublisher&#x3C;Void, Never> {
    userLoggedOutSubject.eraseToAnyPublisher()
}

var cartUpdated: AnyPublisher&#x3C;Cart, Never> {
    cartUpdatedSubject.eraseToAnyPublisher()
}

// Send methods
func send(userLoggedIn user: User) {
    userLoggedInSubject.send(user)
}

func sendUserLoggedOut() {
    userLoggedOutSubject.send()
}

func send(cartUpdated cart: Cart) {
    cartUpdatedSubject.send(cart)
}

}

Quick Reference

Combine vs Async/Await Decision

Scenario Prefer

Single async call async/await

Stream of values Combine

User input debouncing Combine

Combining multiple API calls Either (async let or CombineLatest)

Existing Combine codebase Combine for consistency

New project, simple needs async/await

Subject Comparison

Subject Stores Value New Subscribers Get

PassthroughSubject No Only new values

CurrentValueSubject Yes Current + new values

Common Operators

Category Operators

Transform map, flatMap, compactMap, scan

Filter filter, removeDuplicates, first, dropFirst

Combine combineLatest, zip, merge

Timing debounce, throttle, delay

Error catch, retry, replaceError

Red Flags

Smell Problem Fix

Subscription not stored Immediate cancellation .store(in: &cancellables)

Strong self in sink Retain cycle [weak self]

assign(to:on:self) Retain cycle Use sink or assign(to:&$property)

Public Subject Encapsulation broken Expose as AnyPublisher

flatMap for simple transform Overkill Use map

Debounce on background, sink updates UI Threading bug receive(on: .main)

Empty receiveCompletion Errors ignored Handle .failure case

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

flutter conventions & best practices

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

getx state management patterns

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

ruby oop patterns

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

rails localization (i18n) - english & arabic

No summary provided by upstream source.

Repository SourceNeeds Review